Processing Images by doanchienthangdev/omgkit
npx skills add https://github.com/doanchienthangdev/omgkit --skill 'Processing Images'import sharp from 'sharp';
// 调整大小并针对网络进行优化
async function optimizeImage(inputPath: string, outputPath: string): Promise<void> {
await sharp(inputPath)
.resize(1200, 1200, { fit: 'inside', withoutEnlargement: true })
.webp({ quality: 80 })
.toFile(outputPath);
}
// 使用智能裁剪生成缩略图
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);
}
// 转换为多种格式
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`),
]);
}
| 功能 | 描述 |
|---|
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 指南 |
|---|
| 调整大小 | 使用多种适配模式缩放图像 | 将 resize() 与 cover、contain、fill、inside、outside 配合使用 |
| 格式转换 | 在 JPEG、PNG、WebP、AVIF 之间转换 | 使用 toFormat() 或特定格式的方法 |
| 优化 | 在保持质量的同时减小文件大小 | 设置质量级别并使用 mozjpeg/effort 选项 |
| 智能裁剪 | 自动检测裁剪的焦点 | 使用 sharp.strategy.attention 进行智能定位 |
| 效果 | 应用模糊、锐化、灰度、色调 | 使用 blur()、sharpen()、grayscale()、tint() |
| 水印 | 添加文本或图像叠加层 | 使用 composite() 配合 SVG 或图像缓冲区 |
| 元数据 | 读取 EXIF 数据和图像尺寸 | 使用 metadata() 获取宽度、高度、格式信息 |
| 颜色分析 | 提取主色调 | 使用 raw() 输出配合颜色量化 |
| LQIP 生成 | 创建低质量图像占位符 | 调整到约 20px 并模糊以生成 base64 预览 |
| 批量处理 | 并发处理多个图像 | 使用 p-queue 并控制并发数 |
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',
};
}
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`;
}
// 生成 LQIP 占位符
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 };
}
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);
}
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;
}
| 推荐做法 | 应避免的做法 |
|---|---|
| 对现代浏览器使用 WebP/AVIF 并准备 JPEG 回退 | 对所有浏览器只提供 JPEG/PNG |
| 为懒加载生成 LQIP 占位符 | 在没有占位符的情况下加载完整图像 |
| 缓存处理后的图像以避免重复处理 | 每次请求都重新处理同一图像 |
| 使用 withoutEnlargement 防止放大 | 将图像缩放到大于其原始尺寸 |
| 去除 EXIF 元数据以保护隐私并减小文件 | 在公开图像中暴露 GPS 和相机数据 |
| 在处理前验证图像尺寸和格式 | 未经验证就处理任意文件 |
| 对大图像使用流以减少内存占用 | 将非常大的图像完全加载到内存中 |
| 为网络交付设置适当的品质(70-85) | 过度压缩(低于 60)或压缩不足 |
| 对缩略图使用 sharp.strategy.attention | 对所有图像使用中心裁剪 |
| 为旧版浏览器提供回退格式 | 假设所有浏览器都支持 WebP/AVIF |
每周安装量
0
代码仓库
GitHub 星标数
3
首次出现
1970年1月1日
安全审计
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`),
]);
}
| 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 |
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',
};
}
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 };
}
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);
}
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;
}
| 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 |
Weekly Installs
0
Repository
GitHub Stars
3
First Seen
Jan 1, 1970
Security Audits
GSAP React 动画库使用指南:useGSAP Hook 与最佳实践
1,700 周安装
| Use p-queue with controlled concurrency |