omgkit 2.2.0 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +1 -1
- package/plugin/skills/databases/database-management/SKILL.md +288 -0
- package/plugin/skills/databases/database-migration/SKILL.md +285 -0
- package/plugin/skills/databases/database-schema-design/SKILL.md +195 -0
- package/plugin/skills/databases/mongodb/SKILL.md +60 -776
- package/plugin/skills/databases/prisma/SKILL.md +53 -744
- package/plugin/skills/databases/redis/SKILL.md +53 -860
- package/plugin/skills/databases/supabase/SKILL.md +283 -0
- package/plugin/skills/devops/aws/SKILL.md +68 -672
- package/plugin/skills/devops/github-actions/SKILL.md +54 -657
- package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
- package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
- package/plugin/skills/frameworks/django/SKILL.md +87 -853
- package/plugin/skills/frameworks/express/SKILL.md +95 -1301
- package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
- package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
- package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
- package/plugin/skills/frameworks/react/SKILL.md +94 -962
- package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
- package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
- package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
- package/plugin/skills/frontend/responsive/SKILL.md +76 -799
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
- package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
- package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
- package/plugin/skills/languages/javascript/SKILL.md +106 -849
- package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
- package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
- package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
- package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
- package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
- package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
- package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
- package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
- package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
- package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
- package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
- package/plugin/skills/security/better-auth/SKILL.md +46 -1034
- package/plugin/skills/security/oauth/SKILL.md +80 -934
- package/plugin/skills/security/owasp/SKILL.md +78 -862
- package/plugin/skills/testing/playwright/SKILL.md +77 -700
- package/plugin/skills/testing/pytest/SKILL.md +73 -811
- package/plugin/skills/testing/vitest/SKILL.md +60 -920
- package/plugin/skills/tools/document-processing/SKILL.md +111 -838
- package/plugin/skills/tools/image-processing/SKILL.md +126 -659
- package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
- package/plugin/skills/tools/media-processing/SKILL.md +118 -735
- package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
- package/plugin/skills/SKILL_STANDARDS.md +0 -743
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description:
|
|
2
|
+
name: Processing Images
|
|
3
|
+
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.
|
|
4
4
|
category: tools
|
|
5
5
|
triggers:
|
|
6
6
|
- image processing
|
|
@@ -12,737 +12,204 @@ triggers:
|
|
|
12
12
|
- webp avif
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
-
#
|
|
15
|
+
# Processing Images
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
## Purpose
|
|
20
|
-
|
|
21
|
-
Process images efficiently for web delivery:
|
|
22
|
-
|
|
23
|
-
- Resize and crop images maintaining aspect ratios
|
|
24
|
-
- Convert to modern formats (WebP, AVIF)
|
|
25
|
-
- Optimize file sizes without quality loss
|
|
26
|
-
- Generate responsive image sets
|
|
27
|
-
- Process uploads in batch
|
|
28
|
-
- Apply watermarks and overlays
|
|
29
|
-
|
|
30
|
-
## Features
|
|
31
|
-
|
|
32
|
-
### 1. Basic Image Operations
|
|
17
|
+
## Quick Start
|
|
33
18
|
|
|
34
19
|
```typescript
|
|
35
20
|
import sharp from 'sharp';
|
|
36
|
-
import path from 'path';
|
|
37
|
-
|
|
38
|
-
interface ResizeOptions {
|
|
39
|
-
width?: number;
|
|
40
|
-
height?: number;
|
|
41
|
-
fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
|
|
42
|
-
position?: 'top' | 'right top' | 'right' | 'right bottom' | 'bottom' | 'left bottom' | 'left' | 'left top' | 'center';
|
|
43
|
-
background?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Resize image with options
|
|
47
|
-
async function resizeImage(
|
|
48
|
-
inputPath: string,
|
|
49
|
-
outputPath: string,
|
|
50
|
-
options: ResizeOptions
|
|
51
|
-
): Promise<sharp.OutputInfo> {
|
|
52
|
-
const { width, height, fit = 'cover', position = 'center', background = '#ffffff' } = options;
|
|
53
|
-
|
|
54
|
-
return sharp(inputPath)
|
|
55
|
-
.resize({
|
|
56
|
-
width,
|
|
57
|
-
height,
|
|
58
|
-
fit,
|
|
59
|
-
position,
|
|
60
|
-
background,
|
|
61
|
-
})
|
|
62
|
-
.toFile(outputPath);
|
|
63
|
-
}
|
|
64
21
|
|
|
65
|
-
//
|
|
66
|
-
async function
|
|
67
|
-
inputPath
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
): Promise<sharp.OutputInfo> {
|
|
71
|
-
return sharp(inputPath)
|
|
72
|
-
.extract(region)
|
|
73
|
-
.toFile(outputPath);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Smart crop with attention detection
|
|
77
|
-
async function smartCrop(
|
|
78
|
-
inputPath: string,
|
|
79
|
-
outputPath: string,
|
|
80
|
-
width: number,
|
|
81
|
-
height: number
|
|
82
|
-
): Promise<sharp.OutputInfo> {
|
|
83
|
-
return sharp(inputPath)
|
|
84
|
-
.resize(width, height, {
|
|
85
|
-
fit: 'cover',
|
|
86
|
-
position: sharp.strategy.attention, // Focus on interesting parts
|
|
87
|
-
})
|
|
22
|
+
// Resize and optimize for web
|
|
23
|
+
async function optimizeImage(inputPath: string, outputPath: string): Promise<void> {
|
|
24
|
+
await sharp(inputPath)
|
|
25
|
+
.resize(1200, 1200, { fit: 'inside', withoutEnlargement: true })
|
|
26
|
+
.webp({ quality: 80 })
|
|
88
27
|
.toFile(outputPath);
|
|
89
28
|
}
|
|
90
29
|
|
|
91
|
-
//
|
|
92
|
-
async function
|
|
93
|
-
inputPath
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
): Promise<sharp.OutputInfo> {
|
|
97
|
-
return sharp(inputPath)
|
|
98
|
-
.rotate(angle, { background: { r: 255, g: 255, b: 255, alpha: 0 } })
|
|
30
|
+
// Generate thumbnail with smart crop
|
|
31
|
+
async function generateThumbnail(inputPath: string, outputPath: string): Promise<void> {
|
|
32
|
+
await sharp(inputPath)
|
|
33
|
+
.resize(300, 300, { fit: 'cover', position: sharp.strategy.attention })
|
|
34
|
+
.jpeg({ quality: 85 })
|
|
99
35
|
.toFile(outputPath);
|
|
100
36
|
}
|
|
101
37
|
|
|
102
|
-
//
|
|
103
|
-
async function
|
|
104
|
-
inputPath
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
):
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (direction === 'horizontal') {
|
|
111
|
-
return image.flop().toFile(outputPath);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return image.flip().toFile(outputPath);
|
|
38
|
+
// Convert to multiple formats
|
|
39
|
+
async function convertFormats(inputPath: string, outputDir: string): Promise<void> {
|
|
40
|
+
const baseName = path.basename(inputPath, path.extname(inputPath));
|
|
41
|
+
await Promise.all([
|
|
42
|
+
sharp(inputPath).webp({ quality: 80 }).toFile(`${outputDir}/${baseName}.webp`),
|
|
43
|
+
sharp(inputPath).avif({ quality: 70 }).toFile(`${outputDir}/${baseName}.avif`),
|
|
44
|
+
sharp(inputPath).jpeg({ quality: 85, mozjpeg: true }).toFile(`${outputDir}/${baseName}.jpg`),
|
|
45
|
+
]);
|
|
115
46
|
}
|
|
116
47
|
```
|
|
117
48
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
```typescript
|
|
121
|
-
interface OptimizeOptions {
|
|
122
|
-
quality?: number;
|
|
123
|
-
format?: 'jpeg' | 'png' | 'webp' | 'avif';
|
|
124
|
-
progressive?: boolean;
|
|
125
|
-
stripMetadata?: boolean;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Optimize image for web
|
|
129
|
-
async function optimizeImage(
|
|
130
|
-
inputPath: string,
|
|
131
|
-
outputPath: string,
|
|
132
|
-
options: OptimizeOptions = {}
|
|
133
|
-
): Promise<sharp.OutputInfo> {
|
|
134
|
-
const {
|
|
135
|
-
quality = 80,
|
|
136
|
-
format = 'webp',
|
|
137
|
-
progressive = true,
|
|
138
|
-
stripMetadata = true,
|
|
139
|
-
} = options;
|
|
140
|
-
|
|
141
|
-
let image = sharp(inputPath);
|
|
142
|
-
|
|
143
|
-
if (stripMetadata) {
|
|
144
|
-
image = image.rotate(); // Auto-rotate based on EXIF, then strip
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
switch (format) {
|
|
148
|
-
case 'jpeg':
|
|
149
|
-
return image
|
|
150
|
-
.jpeg({ quality, progressive, mozjpeg: true })
|
|
151
|
-
.toFile(outputPath);
|
|
152
|
-
|
|
153
|
-
case 'png':
|
|
154
|
-
return image
|
|
155
|
-
.png({ quality, compressionLevel: 9, palette: true })
|
|
156
|
-
.toFile(outputPath);
|
|
157
|
-
|
|
158
|
-
case 'webp':
|
|
159
|
-
return image
|
|
160
|
-
.webp({ quality, effort: 6 })
|
|
161
|
-
.toFile(outputPath);
|
|
162
|
-
|
|
163
|
-
case 'avif':
|
|
164
|
-
return image
|
|
165
|
-
.avif({ quality, effort: 6 })
|
|
166
|
-
.toFile(outputPath);
|
|
167
|
-
|
|
168
|
-
default:
|
|
169
|
-
throw new Error(`Unsupported format: ${format}`);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Convert to multiple formats
|
|
174
|
-
async function convertToMultipleFormats(
|
|
175
|
-
inputPath: string,
|
|
176
|
-
outputDir: string,
|
|
177
|
-
formats: OptimizeOptions['format'][] = ['jpeg', 'webp', 'avif']
|
|
178
|
-
): Promise<Map<string, string>> {
|
|
179
|
-
const baseName = path.basename(inputPath, path.extname(inputPath));
|
|
180
|
-
const results = new Map<string, string>();
|
|
49
|
+
## Features
|
|
181
50
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
51
|
+
| Feature | Description | Guide |
|
|
52
|
+
|---------|-------------|-------|
|
|
53
|
+
| Resizing | Scale images with various fit modes | Use resize() with cover, contain, fill, inside, outside |
|
|
54
|
+
| Format Conversion | Convert between JPEG, PNG, WebP, AVIF | Use toFormat() or format-specific methods |
|
|
55
|
+
| Optimization | Reduce file size while preserving quality | Set quality levels and use mozjpeg/effort options |
|
|
56
|
+
| Smart Cropping | Auto-detect focal points for cropping | Use sharp.strategy.attention for smart positioning |
|
|
57
|
+
| Effects | Apply blur, sharpen, grayscale, tint | Use blur(), sharpen(), grayscale(), tint() |
|
|
58
|
+
| Watermarks | Add text or image overlays | Use composite() with SVG or image buffers |
|
|
59
|
+
| Metadata | Read EXIF data and image dimensions | Use metadata() for width, height, format info |
|
|
60
|
+
| Color Analysis | Extract dominant colors | Use raw() output with color quantization |
|
|
61
|
+
| LQIP Generation | Create low-quality image placeholders | Resize to ~20px with blur for base64 preview |
|
|
62
|
+
| Batch Processing | Process multiple images concurrently | Use p-queue with controlled concurrency |
|
|
189
63
|
|
|
190
|
-
|
|
191
|
-
}
|
|
64
|
+
## Common Patterns
|
|
192
65
|
|
|
193
|
-
|
|
194
|
-
interface SrcsetOptions {
|
|
195
|
-
widths: number[];
|
|
196
|
-
formats: ('jpeg' | 'webp' | 'avif')[];
|
|
197
|
-
quality?: number;
|
|
198
|
-
}
|
|
66
|
+
### Responsive Image Set Generation
|
|
199
67
|
|
|
200
|
-
|
|
68
|
+
```typescript
|
|
69
|
+
async function generateResponsiveSet(
|
|
201
70
|
inputPath: string,
|
|
202
71
|
outputDir: string,
|
|
203
|
-
|
|
204
|
-
): Promise<{
|
|
205
|
-
sources: Array<{ srcset: string; type: string }>;
|
|
206
|
-
fallback: string;
|
|
207
|
-
}> {
|
|
208
|
-
const { widths, formats, quality = 80 } = options;
|
|
72
|
+
widths: number[] = [320, 640, 1024, 1920]
|
|
73
|
+
): Promise<{ srcset: string; sizes: string }> {
|
|
209
74
|
const baseName = path.basename(inputPath, path.extname(inputPath));
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
for (const format of formats) {
|
|
213
|
-
const srcsetParts: string[] = [];
|
|
75
|
+
const srcsetParts: string[] = [];
|
|
214
76
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
.toFile(outputPath);
|
|
223
|
-
|
|
224
|
-
srcsetParts.push(`${filename} ${width}w`);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
sources.push({
|
|
228
|
-
srcset: srcsetParts.join(', '),
|
|
229
|
-
type: `image/${format}`,
|
|
230
|
-
});
|
|
77
|
+
for (const width of widths) {
|
|
78
|
+
const filename = `${baseName}-${width}w.webp`;
|
|
79
|
+
await sharp(inputPath)
|
|
80
|
+
.resize(width, null, { withoutEnlargement: true })
|
|
81
|
+
.webp({ quality: 80 })
|
|
82
|
+
.toFile(path.join(outputDir, filename));
|
|
83
|
+
srcsetParts.push(`${filename} ${width}w`);
|
|
231
84
|
}
|
|
232
85
|
|
|
233
|
-
// Generate fallback JPEG
|
|
234
|
-
const fallbackPath = path.join(outputDir, `${baseName}-fallback.jpg`);
|
|
235
|
-
await sharp(inputPath)
|
|
236
|
-
.resize(widths[widths.length - 1])
|
|
237
|
-
.jpeg({ quality })
|
|
238
|
-
.toFile(fallbackPath);
|
|
239
|
-
|
|
240
86
|
return {
|
|
241
|
-
|
|
242
|
-
|
|
87
|
+
srcset: srcsetParts.join(', '),
|
|
88
|
+
sizes: '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw',
|
|
243
89
|
};
|
|
244
90
|
}
|
|
245
91
|
```
|
|
246
92
|
|
|
247
|
-
###
|
|
93
|
+
### E-commerce Product Image Processing
|
|
248
94
|
|
|
249
95
|
```typescript
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
outputPath: string,
|
|
254
|
-
sigma: number = 10
|
|
255
|
-
): Promise<sharp.OutputInfo> {
|
|
256
|
-
return sharp(inputPath)
|
|
257
|
-
.blur(sigma)
|
|
258
|
-
.toFile(outputPath);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Sharpen image
|
|
262
|
-
async function sharpenImage(
|
|
263
|
-
inputPath: string,
|
|
264
|
-
outputPath: string,
|
|
265
|
-
options: { sigma?: number; flat?: number; jagged?: number } = {}
|
|
266
|
-
): Promise<sharp.OutputInfo> {
|
|
267
|
-
const { sigma = 1, flat = 1, jagged = 2 } = options;
|
|
268
|
-
|
|
269
|
-
return sharp(inputPath)
|
|
270
|
-
.sharpen(sigma, flat, jagged)
|
|
271
|
-
.toFile(outputPath);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Adjust colors
|
|
275
|
-
interface ColorAdjustments {
|
|
276
|
-
brightness?: number; // 0.5 to 2
|
|
277
|
-
saturation?: number; // 0 to 2
|
|
278
|
-
hue?: number; // 0 to 360
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
async function adjustColors(
|
|
282
|
-
inputPath: string,
|
|
283
|
-
outputPath: string,
|
|
284
|
-
adjustments: ColorAdjustments
|
|
285
|
-
): Promise<sharp.OutputInfo> {
|
|
286
|
-
const { brightness = 1, saturation = 1, hue = 0 } = adjustments;
|
|
287
|
-
|
|
288
|
-
return sharp(inputPath)
|
|
289
|
-
.modulate({
|
|
290
|
-
brightness,
|
|
291
|
-
saturation,
|
|
292
|
-
hue,
|
|
293
|
-
})
|
|
294
|
-
.toFile(outputPath);
|
|
295
|
-
}
|
|
96
|
+
async function processProductImage(inputPath: string, productId: string): Promise<ProductImages> {
|
|
97
|
+
const outputDir = path.join(MEDIA_DIR, 'products', productId);
|
|
98
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
296
99
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
.grayscale()
|
|
304
|
-
.toFile(outputPath);
|
|
305
|
-
}
|
|
100
|
+
const sizes = [
|
|
101
|
+
{ name: 'thumb', width: 150, height: 150 },
|
|
102
|
+
{ name: 'small', width: 300, height: 300 },
|
|
103
|
+
{ name: 'medium', width: 600, height: 600 },
|
|
104
|
+
{ name: 'large', width: 1200, height: 1200 },
|
|
105
|
+
];
|
|
306
106
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
.
|
|
315
|
-
|
|
316
|
-
}
|
|
107
|
+
const images: Record<string, string> = {};
|
|
108
|
+
for (const size of sizes) {
|
|
109
|
+
const outputPath = path.join(outputDir, `${size.name}.webp`);
|
|
110
|
+
await sharp(inputPath)
|
|
111
|
+
.resize(size.width, size.height, { fit: 'contain', background: '#ffffff' })
|
|
112
|
+
.webp({ quality: 85 })
|
|
113
|
+
.toFile(outputPath);
|
|
114
|
+
images[size.name] = `/media/products/${productId}/${size.name}.webp`;
|
|
115
|
+
}
|
|
317
116
|
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
outputPath: string
|
|
322
|
-
): Promise<{ dataUri: string; width: number; height: number }> {
|
|
323
|
-
const image = sharp(inputPath);
|
|
324
|
-
const metadata = await image.metadata();
|
|
325
|
-
|
|
326
|
-
const buffer = await image
|
|
327
|
-
.resize(20) // Tiny size
|
|
328
|
-
.blur(5)
|
|
329
|
-
.jpeg({ quality: 20 })
|
|
330
|
-
.toBuffer();
|
|
117
|
+
// Generate LQIP placeholder
|
|
118
|
+
const lqipBuffer = await sharp(inputPath).resize(20).blur(5).jpeg({ quality: 20 }).toBuffer();
|
|
119
|
+
const lqip = `data:image/jpeg;base64,${lqipBuffer.toString('base64')}`;
|
|
331
120
|
|
|
332
|
-
return {
|
|
333
|
-
dataUri: `data:image/jpeg;base64,${buffer.toString('base64')}`,
|
|
334
|
-
width: metadata.width || 0,
|
|
335
|
-
height: metadata.height || 0,
|
|
336
|
-
};
|
|
121
|
+
return { images, lqip };
|
|
337
122
|
}
|
|
338
123
|
```
|
|
339
124
|
|
|
340
|
-
###
|
|
125
|
+
### Image Watermarking
|
|
341
126
|
|
|
342
127
|
```typescript
|
|
343
|
-
|
|
344
|
-
async function addTextWatermark(
|
|
345
|
-
inputPath: string,
|
|
346
|
-
outputPath: string,
|
|
347
|
-
text: string,
|
|
348
|
-
options: {
|
|
349
|
-
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center';
|
|
350
|
-
fontSize?: number;
|
|
351
|
-
color?: string;
|
|
352
|
-
opacity?: number;
|
|
353
|
-
} = {}
|
|
354
|
-
): Promise<sharp.OutputInfo> {
|
|
355
|
-
const {
|
|
356
|
-
position = 'bottom-right',
|
|
357
|
-
fontSize = 24,
|
|
358
|
-
color = '#ffffff',
|
|
359
|
-
opacity = 0.5,
|
|
360
|
-
} = options;
|
|
361
|
-
|
|
128
|
+
async function addWatermark(inputPath: string, outputPath: string, watermarkPath: string): Promise<void> {
|
|
362
129
|
const metadata = await sharp(inputPath).metadata();
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
// Create SVG text overlay
|
|
366
|
-
const svg = `
|
|
367
|
-
<svg width="${width}" height="${height}">
|
|
368
|
-
<style>
|
|
369
|
-
.watermark {
|
|
370
|
-
fill: ${color};
|
|
371
|
-
font-size: ${fontSize}px;
|
|
372
|
-
font-family: Arial, sans-serif;
|
|
373
|
-
opacity: ${opacity};
|
|
374
|
-
}
|
|
375
|
-
</style>
|
|
376
|
-
<text
|
|
377
|
-
x="${getXPosition(position, width, fontSize * text.length * 0.6)}"
|
|
378
|
-
y="${getYPosition(position, height, fontSize)}"
|
|
379
|
-
class="watermark"
|
|
380
|
-
>${text}</text>
|
|
381
|
-
</svg>
|
|
382
|
-
`;
|
|
383
|
-
|
|
384
|
-
return sharp(inputPath)
|
|
385
|
-
.composite([{
|
|
386
|
-
input: Buffer.from(svg),
|
|
387
|
-
gravity: positionToGravity(position),
|
|
388
|
-
}])
|
|
389
|
-
.toFile(outputPath);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Add image watermark/overlay
|
|
393
|
-
async function addImageOverlay(
|
|
394
|
-
inputPath: string,
|
|
395
|
-
overlayPath: string,
|
|
396
|
-
outputPath: string,
|
|
397
|
-
options: {
|
|
398
|
-
position?: sharp.Gravity;
|
|
399
|
-
opacity?: number;
|
|
400
|
-
blend?: sharp.Blend;
|
|
401
|
-
scale?: number;
|
|
402
|
-
} = {}
|
|
403
|
-
): Promise<sharp.OutputInfo> {
|
|
404
|
-
const {
|
|
405
|
-
position = 'southeast',
|
|
406
|
-
opacity = 0.8,
|
|
407
|
-
blend = 'over',
|
|
408
|
-
scale,
|
|
409
|
-
} = options;
|
|
410
|
-
|
|
411
|
-
let overlay = sharp(overlayPath);
|
|
412
|
-
|
|
413
|
-
if (scale) {
|
|
414
|
-
const overlayMeta = await overlay.metadata();
|
|
415
|
-
overlay = overlay.resize(
|
|
416
|
-
Math.round((overlayMeta.width || 100) * scale),
|
|
417
|
-
Math.round((overlayMeta.height || 100) * scale)
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const overlayBuffer = await overlay
|
|
422
|
-
.ensureAlpha(opacity)
|
|
130
|
+
const watermark = await sharp(watermarkPath)
|
|
131
|
+
.resize(Math.round((metadata.width || 800) * 0.2))
|
|
423
132
|
.toBuffer();
|
|
424
133
|
|
|
425
|
-
|
|
426
|
-
.composite([{
|
|
427
|
-
input: overlayBuffer,
|
|
428
|
-
gravity: position,
|
|
429
|
-
blend,
|
|
430
|
-
}])
|
|
431
|
-
.toFile(outputPath);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Create image collage
|
|
435
|
-
async function createCollage(
|
|
436
|
-
images: string[],
|
|
437
|
-
outputPath: string,
|
|
438
|
-
options: {
|
|
439
|
-
columns: number;
|
|
440
|
-
tileWidth: number;
|
|
441
|
-
tileHeight: number;
|
|
442
|
-
gap?: number;
|
|
443
|
-
background?: string;
|
|
444
|
-
}
|
|
445
|
-
): Promise<sharp.OutputInfo> {
|
|
446
|
-
const { columns, tileWidth, tileHeight, gap = 0, background = '#ffffff' } = options;
|
|
447
|
-
const rows = Math.ceil(images.length / columns);
|
|
448
|
-
|
|
449
|
-
const totalWidth = columns * tileWidth + (columns - 1) * gap;
|
|
450
|
-
const totalHeight = rows * tileHeight + (rows - 1) * gap;
|
|
451
|
-
|
|
452
|
-
// Create base canvas
|
|
453
|
-
const canvas = sharp({
|
|
454
|
-
create: {
|
|
455
|
-
width: totalWidth,
|
|
456
|
-
height: totalHeight,
|
|
457
|
-
channels: 3,
|
|
458
|
-
background,
|
|
459
|
-
},
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
// Prepare tiles
|
|
463
|
-
const composites: sharp.OverlayOptions[] = await Promise.all(
|
|
464
|
-
images.map(async (imagePath, index) => {
|
|
465
|
-
const col = index % columns;
|
|
466
|
-
const row = Math.floor(index / columns);
|
|
467
|
-
|
|
468
|
-
const buffer = await sharp(imagePath)
|
|
469
|
-
.resize(tileWidth, tileHeight, { fit: 'cover' })
|
|
470
|
-
.toBuffer();
|
|
471
|
-
|
|
472
|
-
return {
|
|
473
|
-
input: buffer,
|
|
474
|
-
left: col * (tileWidth + gap),
|
|
475
|
-
top: row * (tileHeight + gap),
|
|
476
|
-
};
|
|
477
|
-
})
|
|
478
|
-
);
|
|
479
|
-
|
|
480
|
-
return canvas
|
|
481
|
-
.composite(composites)
|
|
482
|
-
.jpeg({ quality: 90 })
|
|
134
|
+
await sharp(inputPath)
|
|
135
|
+
.composite([{ input: watermark, gravity: 'southeast', blend: 'over' }])
|
|
483
136
|
.toFile(outputPath);
|
|
484
137
|
}
|
|
485
|
-
```
|
|
486
|
-
|
|
487
|
-
### 5. Metadata & Analysis
|
|
488
|
-
|
|
489
|
-
```typescript
|
|
490
|
-
interface ImageInfo {
|
|
491
|
-
width: number;
|
|
492
|
-
height: number;
|
|
493
|
-
format: string;
|
|
494
|
-
size: number;
|
|
495
|
-
hasAlpha: boolean;
|
|
496
|
-
orientation?: number;
|
|
497
|
-
colorSpace?: string;
|
|
498
|
-
exif?: Record<string, any>;
|
|
499
|
-
}
|
|
500
138
|
|
|
501
|
-
|
|
502
|
-
async function getImageInfo(inputPath: string): Promise<ImageInfo> {
|
|
139
|
+
async function addTextWatermark(inputPath: string, outputPath: string, text: string): Promise<void> {
|
|
503
140
|
const metadata = await sharp(inputPath).metadata();
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
return {
|
|
507
|
-
width: metadata.width || 0,
|
|
508
|
-
height: metadata.height || 0,
|
|
509
|
-
format: metadata.format || 'unknown',
|
|
510
|
-
size: stats.size,
|
|
511
|
-
hasAlpha: metadata.hasAlpha || false,
|
|
512
|
-
orientation: metadata.orientation,
|
|
513
|
-
colorSpace: metadata.space,
|
|
514
|
-
exif: metadata.exif ? parseExif(metadata.exif) : undefined,
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Extract dominant colors
|
|
519
|
-
async function getDominantColors(
|
|
520
|
-
inputPath: string,
|
|
521
|
-
count: number = 5
|
|
522
|
-
): Promise<string[]> {
|
|
523
|
-
const { data, info } = await sharp(inputPath)
|
|
524
|
-
.resize(100, 100, { fit: 'cover' })
|
|
525
|
-
.raw()
|
|
526
|
-
.toBuffer({ resolveWithObject: true });
|
|
527
|
-
|
|
528
|
-
const colors = extractColors(data, info.width, info.height, count);
|
|
529
|
-
|
|
530
|
-
return colors.map(([r, g, b]) =>
|
|
531
|
-
`#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
|
|
532
|
-
);
|
|
533
|
-
}
|
|
141
|
+
const { width = 800, height = 600 } = metadata;
|
|
534
142
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
contrast: number;
|
|
540
|
-
isBlurry: boolean;
|
|
541
|
-
}> {
|
|
542
|
-
const { data, info } = await sharp(inputPath)
|
|
543
|
-
.resize(200) // Analyze at smaller size for speed
|
|
544
|
-
.greyscale()
|
|
545
|
-
.raw()
|
|
546
|
-
.toBuffer({ resolveWithObject: true });
|
|
547
|
-
|
|
548
|
-
// Calculate Laplacian variance (blur detection)
|
|
549
|
-
const laplacianVariance = calculateLaplacianVariance(data, info.width, info.height);
|
|
550
|
-
|
|
551
|
-
// Calculate other metrics
|
|
552
|
-
const { mean, stddev } = calculateStats(data);
|
|
143
|
+
const svg = `<svg width="${width}" height="${height}">
|
|
144
|
+
<text x="${width - 20}" y="${height - 20}" text-anchor="end"
|
|
145
|
+
font-size="24" fill="white" opacity="0.5">${text}</text>
|
|
146
|
+
</svg>`;
|
|
553
147
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
contrast: stddev / 128,
|
|
558
|
-
isBlurry: laplacianVariance < 100, // Threshold for blur detection
|
|
559
|
-
};
|
|
148
|
+
await sharp(inputPath)
|
|
149
|
+
.composite([{ input: Buffer.from(svg), gravity: 'southeast' }])
|
|
150
|
+
.toFile(outputPath);
|
|
560
151
|
}
|
|
561
152
|
```
|
|
562
153
|
|
|
563
|
-
###
|
|
154
|
+
### Batch Processing with Progress
|
|
564
155
|
|
|
565
156
|
```typescript
|
|
566
157
|
import PQueue from 'p-queue';
|
|
567
158
|
|
|
568
|
-
interface BatchProcessOptions {
|
|
569
|
-
concurrency?: number;
|
|
570
|
-
outputDir: string;
|
|
571
|
-
transform: (image: sharp.Sharp, filename: string) => sharp.Sharp;
|
|
572
|
-
format?: 'jpeg' | 'png' | 'webp' | 'avif';
|
|
573
|
-
quality?: number;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
159
|
async function batchProcessImages(
|
|
577
160
|
inputPaths: string[],
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const queue = new PQueue({ concurrency });
|
|
589
|
-
const results = new Map<string, { success: boolean; outputPath?: string; error?: string }>();
|
|
590
|
-
|
|
591
|
-
await fs.mkdir(outputDir, { recursive: true });
|
|
592
|
-
|
|
593
|
-
const tasks = inputPaths.map((inputPath) =>
|
|
161
|
+
outputDir: string,
|
|
162
|
+
transform: (image: sharp.Sharp) => sharp.Sharp,
|
|
163
|
+
onProgress?: (completed: number, total: number) => void
|
|
164
|
+
): Promise<Map<string, { success: boolean; error?: string }>> {
|
|
165
|
+
const queue = new PQueue({ concurrency: 4 });
|
|
166
|
+
const results = new Map<string, { success: boolean; error?: string }>();
|
|
167
|
+
let completed = 0;
|
|
168
|
+
|
|
169
|
+
for (const inputPath of inputPaths) {
|
|
594
170
|
queue.add(async () => {
|
|
595
|
-
const filename = path.basename(inputPath, path.extname(inputPath));
|
|
596
|
-
const outputPath = path.join(outputDir, `${filename}.${format}`);
|
|
597
|
-
|
|
171
|
+
const filename = path.basename(inputPath, path.extname(inputPath)) + '.webp';
|
|
598
172
|
try {
|
|
599
173
|
let image = sharp(inputPath);
|
|
600
|
-
image = transform(image
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
results.set(inputPath, { success: true, outputPath });
|
|
174
|
+
image = transform(image);
|
|
175
|
+
await image.toFile(path.join(outputDir, filename));
|
|
176
|
+
results.set(inputPath, { success: true });
|
|
605
177
|
} catch (error) {
|
|
606
|
-
results.set(inputPath, {
|
|
607
|
-
success: false,
|
|
608
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
609
|
-
});
|
|
178
|
+
results.set(inputPath, { success: false, error: error.message });
|
|
610
179
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
await Promise.all(tasks);
|
|
615
|
-
|
|
616
|
-
return results;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// Example: Batch resize and optimize
|
|
620
|
-
async function processUploads(uploadPaths: string[]): Promise<void> {
|
|
621
|
-
const results = await batchProcessImages(uploadPaths, {
|
|
622
|
-
outputDir: './processed',
|
|
623
|
-
concurrency: 4,
|
|
624
|
-
format: 'webp',
|
|
625
|
-
quality: 80,
|
|
626
|
-
transform: (image) => image
|
|
627
|
-
.resize(1200, 1200, { fit: 'inside', withoutEnlargement: true })
|
|
628
|
-
.sharpen(),
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
for (const [input, result] of results) {
|
|
632
|
-
if (result.success) {
|
|
633
|
-
console.log(`Processed: ${input} -> ${result.outputPath}`);
|
|
634
|
-
} else {
|
|
635
|
-
console.error(`Failed: ${input} - ${result.error}`);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
```
|
|
640
|
-
|
|
641
|
-
## Use Cases
|
|
642
|
-
|
|
643
|
-
### 1. E-commerce Product Images
|
|
644
|
-
|
|
645
|
-
```typescript
|
|
646
|
-
// Process product image with all variants
|
|
647
|
-
async function processProductImage(
|
|
648
|
-
inputPath: string,
|
|
649
|
-
productId: string
|
|
650
|
-
): Promise<ProductImageSet> {
|
|
651
|
-
const outputDir = path.join(MEDIA_DIR, 'products', productId);
|
|
652
|
-
await fs.mkdir(outputDir, { recursive: true });
|
|
653
|
-
|
|
654
|
-
const sizes = [
|
|
655
|
-
{ name: 'thumb', width: 150, height: 150 },
|
|
656
|
-
{ name: 'small', width: 300, height: 300 },
|
|
657
|
-
{ name: 'medium', width: 600, height: 600 },
|
|
658
|
-
{ name: 'large', width: 1200, height: 1200 },
|
|
659
|
-
];
|
|
660
|
-
|
|
661
|
-
const images: Record<string, Record<string, string>> = {};
|
|
662
|
-
|
|
663
|
-
for (const size of sizes) {
|
|
664
|
-
images[size.name] = {};
|
|
665
|
-
|
|
666
|
-
for (const format of ['webp', 'jpeg'] as const) {
|
|
667
|
-
const outputPath = path.join(outputDir, `${size.name}.${format}`);
|
|
668
|
-
|
|
669
|
-
await sharp(inputPath)
|
|
670
|
-
.resize(size.width, size.height, { fit: 'contain', background: '#ffffff' })
|
|
671
|
-
.toFormat(format, { quality: format === 'webp' ? 85 : 90 })
|
|
672
|
-
.toFile(outputPath);
|
|
673
|
-
|
|
674
|
-
images[size.name][format] = `/media/products/${productId}/${size.name}.${format}`;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// Generate LQIP
|
|
679
|
-
const lqip = await createLQIP(inputPath, path.join(outputDir, 'lqip.jpg'));
|
|
680
|
-
|
|
681
|
-
return { images, lqip: lqip.dataUri };
|
|
682
|
-
}
|
|
683
|
-
```
|
|
684
|
-
|
|
685
|
-
### 2. User Avatar Processing
|
|
686
|
-
|
|
687
|
-
```typescript
|
|
688
|
-
// Process avatar upload
|
|
689
|
-
async function processAvatar(
|
|
690
|
-
inputPath: string,
|
|
691
|
-
userId: string
|
|
692
|
-
): Promise<AvatarSet> {
|
|
693
|
-
const outputDir = path.join(MEDIA_DIR, 'avatars', userId);
|
|
694
|
-
await fs.mkdir(outputDir, { recursive: true });
|
|
695
|
-
|
|
696
|
-
const sizes = [32, 64, 128, 256];
|
|
697
|
-
const urls: Record<number, string> = {};
|
|
698
|
-
|
|
699
|
-
for (const size of sizes) {
|
|
700
|
-
const outputPath = path.join(outputDir, `${size}.webp`);
|
|
701
|
-
|
|
702
|
-
await sharp(inputPath)
|
|
703
|
-
.resize(size, size, {
|
|
704
|
-
fit: 'cover',
|
|
705
|
-
position: sharp.strategy.attention,
|
|
706
|
-
})
|
|
707
|
-
.webp({ quality: 90 })
|
|
708
|
-
.toFile(outputPath);
|
|
709
|
-
|
|
710
|
-
urls[size] = `/media/avatars/${userId}/${size}.webp`;
|
|
180
|
+
completed++;
|
|
181
|
+
onProgress?.(completed, inputPaths.length);
|
|
182
|
+
});
|
|
711
183
|
}
|
|
712
184
|
|
|
713
|
-
|
|
185
|
+
await queue.onIdle();
|
|
186
|
+
return results;
|
|
714
187
|
}
|
|
715
188
|
```
|
|
716
189
|
|
|
717
190
|
## Best Practices
|
|
718
191
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
- Don't over-compress (quality < 60 shows artifacts)
|
|
732
|
-
- Don't process without error handling
|
|
733
|
-
- Don't ignore color profiles
|
|
734
|
-
- Don't skip responsive images
|
|
735
|
-
- Don't forget fallbacks for older browsers
|
|
192
|
+
| Do | Avoid |
|
|
193
|
+
|----|-------|
|
|
194
|
+
| Use WebP/AVIF for modern browsers with JPEG fallback | Serving only JPEG/PNG to all browsers |
|
|
195
|
+
| Generate LQIP placeholders for lazy loading | Loading full images without placeholders |
|
|
196
|
+
| Cache processed images to avoid reprocessing | Re-processing the same image on each request |
|
|
197
|
+
| Use withoutEnlargement to prevent upscaling | Scaling images larger than their original size |
|
|
198
|
+
| Strip EXIF metadata for privacy and smaller files | Exposing GPS and camera data in public images |
|
|
199
|
+
| Validate image dimensions and format before processing | Processing arbitrary files without validation |
|
|
200
|
+
| Use streams for large images to reduce memory | Loading very large images entirely into memory |
|
|
201
|
+
| Set appropriate quality (70-85) for web delivery | Over-compressing (below 60) or under-compressing |
|
|
202
|
+
| Use sharp.strategy.attention for thumbnails | Using center crop for all images |
|
|
203
|
+
| Provide fallback formats for older browsers | Assuming all browsers support WebP/AVIF |
|
|
736
204
|
|
|
737
205
|
## Related Skills
|
|
738
206
|
|
|
739
|
-
- **media-processing** - Video processing
|
|
740
|
-
- **frontend-design** - Image usage in UI
|
|
741
|
-
- **performance-profiling** - Image impact on performance
|
|
207
|
+
- **media-processing** - Video and audio processing
|
|
208
|
+
- **frontend-design** - Image usage in UI design
|
|
742
209
|
|
|
743
|
-
##
|
|
210
|
+
## References
|
|
744
211
|
|
|
745
212
|
- [Sharp Documentation](https://sharp.pixelplumbing.com/)
|
|
746
213
|
- [Web.dev Image Optimization](https://web.dev/fast/#optimize-your-images)
|
|
747
|
-
- [Squoosh](https://squoosh.app/) - Format comparison
|
|
748
|
-
- [AVIF
|
|
214
|
+
- [Squoosh](https://squoosh.app/) - Format comparison tool
|
|
215
|
+
- [Can I Use AVIF](https://caniuse.com/avif)
|