omgkit 2.0.7 → 2.1.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/package.json +2 -2
- package/plugin/skills/backend/api-architecture/SKILL.md +857 -0
- package/plugin/skills/backend/caching-strategies/SKILL.md +755 -0
- package/plugin/skills/backend/event-driven-architecture/SKILL.md +753 -0
- package/plugin/skills/backend/real-time-systems/SKILL.md +635 -0
- package/plugin/skills/databases/database-optimization/SKILL.md +571 -0
- package/plugin/skills/databases/postgresql/SKILL.md +494 -18
- package/plugin/skills/devops/docker/SKILL.md +466 -18
- package/plugin/skills/devops/monorepo-management/SKILL.md +595 -0
- package/plugin/skills/devops/observability/SKILL.md +622 -0
- package/plugin/skills/devops/performance-profiling/SKILL.md +905 -0
- package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
- package/plugin/skills/frameworks/react/SKILL.md +1006 -32
- package/plugin/skills/frontend/advanced-ui-design/SKILL.md +426 -0
- package/plugin/skills/integrations/ai-integration/SKILL.md +730 -0
- package/plugin/skills/integrations/payment-integration/SKILL.md +735 -0
- package/plugin/skills/languages/python/SKILL.md +489 -25
- package/plugin/skills/languages/typescript/SKILL.md +379 -30
- package/plugin/skills/methodology/problem-solving/SKILL.md +355 -0
- package/plugin/skills/methodology/research-validation/SKILL.md +668 -0
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +260 -0
- package/plugin/skills/mobile/mobile-development/SKILL.md +756 -0
- package/plugin/skills/security/security-hardening/SKILL.md +633 -0
- package/plugin/skills/tools/document-processing/SKILL.md +916 -0
- package/plugin/skills/tools/image-processing/SKILL.md +748 -0
- package/plugin/skills/tools/mcp-development/SKILL.md +883 -0
- package/plugin/skills/tools/media-processing/SKILL.md +831 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: image-processing
|
|
3
|
+
description: Enterprise image manipulation with Sharp including optimization, resizing, format conversion, and batch operations
|
|
4
|
+
category: tools
|
|
5
|
+
triggers:
|
|
6
|
+
- image processing
|
|
7
|
+
- sharp
|
|
8
|
+
- image optimization
|
|
9
|
+
- resize images
|
|
10
|
+
- image conversion
|
|
11
|
+
- thumbnail generation
|
|
12
|
+
- webp avif
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Image Processing
|
|
16
|
+
|
|
17
|
+
High-performance **image processing** with Sharp. This skill covers optimization, resizing, format conversion, and batch processing for web applications.
|
|
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
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
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
|
+
|
|
65
|
+
// Crop to specific dimensions
|
|
66
|
+
async function cropImage(
|
|
67
|
+
inputPath: string,
|
|
68
|
+
outputPath: string,
|
|
69
|
+
region: { left: number; top: number; width: number; height: number }
|
|
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
|
+
})
|
|
88
|
+
.toFile(outputPath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Rotate image
|
|
92
|
+
async function rotateImage(
|
|
93
|
+
inputPath: string,
|
|
94
|
+
outputPath: string,
|
|
95
|
+
angle: number
|
|
96
|
+
): Promise<sharp.OutputInfo> {
|
|
97
|
+
return sharp(inputPath)
|
|
98
|
+
.rotate(angle, { background: { r: 255, g: 255, b: 255, alpha: 0 } })
|
|
99
|
+
.toFile(outputPath);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Flip and flop
|
|
103
|
+
async function flipImage(
|
|
104
|
+
inputPath: string,
|
|
105
|
+
outputPath: string,
|
|
106
|
+
direction: 'horizontal' | 'vertical'
|
|
107
|
+
): Promise<sharp.OutputInfo> {
|
|
108
|
+
const image = sharp(inputPath);
|
|
109
|
+
|
|
110
|
+
if (direction === 'horizontal') {
|
|
111
|
+
return image.flop().toFile(outputPath);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return image.flip().toFile(outputPath);
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 2. Format Conversion & Optimization
|
|
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>();
|
|
181
|
+
|
|
182
|
+
await Promise.all(
|
|
183
|
+
formats.map(async (format) => {
|
|
184
|
+
const outputPath = path.join(outputDir, `${baseName}.${format}`);
|
|
185
|
+
await optimizeImage(inputPath, outputPath, { format });
|
|
186
|
+
results.set(format!, outputPath);
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
return results;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Generate srcset for responsive images
|
|
194
|
+
interface SrcsetOptions {
|
|
195
|
+
widths: number[];
|
|
196
|
+
formats: ('jpeg' | 'webp' | 'avif')[];
|
|
197
|
+
quality?: number;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function generateSrcset(
|
|
201
|
+
inputPath: string,
|
|
202
|
+
outputDir: string,
|
|
203
|
+
options: SrcsetOptions
|
|
204
|
+
): Promise<{
|
|
205
|
+
sources: Array<{ srcset: string; type: string }>;
|
|
206
|
+
fallback: string;
|
|
207
|
+
}> {
|
|
208
|
+
const { widths, formats, quality = 80 } = options;
|
|
209
|
+
const baseName = path.basename(inputPath, path.extname(inputPath));
|
|
210
|
+
const sources: Array<{ srcset: string; type: string }> = [];
|
|
211
|
+
|
|
212
|
+
for (const format of formats) {
|
|
213
|
+
const srcsetParts: string[] = [];
|
|
214
|
+
|
|
215
|
+
for (const width of widths) {
|
|
216
|
+
const filename = `${baseName}-${width}w.${format}`;
|
|
217
|
+
const outputPath = path.join(outputDir, filename);
|
|
218
|
+
|
|
219
|
+
await sharp(inputPath)
|
|
220
|
+
.resize(width)
|
|
221
|
+
.toFormat(format, { quality })
|
|
222
|
+
.toFile(outputPath);
|
|
223
|
+
|
|
224
|
+
srcsetParts.push(`${filename} ${width}w`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
sources.push({
|
|
228
|
+
srcset: srcsetParts.join(', '),
|
|
229
|
+
type: `image/${format}`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
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
|
+
return {
|
|
241
|
+
sources,
|
|
242
|
+
fallback: `${baseName}-fallback.jpg`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 3. Image Effects & Filters
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// Apply blur effect
|
|
251
|
+
async function blurImage(
|
|
252
|
+
inputPath: string,
|
|
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
|
+
}
|
|
296
|
+
|
|
297
|
+
// Apply grayscale
|
|
298
|
+
async function grayscale(
|
|
299
|
+
inputPath: string,
|
|
300
|
+
outputPath: string
|
|
301
|
+
): Promise<sharp.OutputInfo> {
|
|
302
|
+
return sharp(inputPath)
|
|
303
|
+
.grayscale()
|
|
304
|
+
.toFile(outputPath);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Apply tint
|
|
308
|
+
async function tintImage(
|
|
309
|
+
inputPath: string,
|
|
310
|
+
outputPath: string,
|
|
311
|
+
color: string // hex color
|
|
312
|
+
): Promise<sharp.OutputInfo> {
|
|
313
|
+
return sharp(inputPath)
|
|
314
|
+
.tint(color)
|
|
315
|
+
.toFile(outputPath);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Create placeholder blur (LQIP)
|
|
319
|
+
async function createLQIP(
|
|
320
|
+
inputPath: string,
|
|
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();
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
dataUri: `data:image/jpeg;base64,${buffer.toString('base64')}`,
|
|
334
|
+
width: metadata.width || 0,
|
|
335
|
+
height: metadata.height || 0,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### 4. Watermarks & Overlays
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
// Add text watermark
|
|
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
|
+
|
|
362
|
+
const metadata = await sharp(inputPath).metadata();
|
|
363
|
+
const { width = 800, height = 600 } = metadata;
|
|
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)
|
|
423
|
+
.toBuffer();
|
|
424
|
+
|
|
425
|
+
return sharp(inputPath)
|
|
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 })
|
|
483
|
+
.toFile(outputPath);
|
|
484
|
+
}
|
|
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
|
+
|
|
501
|
+
// Get comprehensive image info
|
|
502
|
+
async function getImageInfo(inputPath: string): Promise<ImageInfo> {
|
|
503
|
+
const metadata = await sharp(inputPath).metadata();
|
|
504
|
+
const stats = await fs.stat(inputPath);
|
|
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
|
+
}
|
|
534
|
+
|
|
535
|
+
// Analyze image for blur/quality
|
|
536
|
+
async function analyzeImageQuality(inputPath: string): Promise<{
|
|
537
|
+
sharpness: number;
|
|
538
|
+
brightness: number;
|
|
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);
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
sharpness: laplacianVariance,
|
|
556
|
+
brightness: mean / 255,
|
|
557
|
+
contrast: stddev / 128,
|
|
558
|
+
isBlurry: laplacianVariance < 100, // Threshold for blur detection
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### 6. Batch Processing
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
import PQueue from 'p-queue';
|
|
567
|
+
|
|
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
|
+
async function batchProcessImages(
|
|
577
|
+
inputPaths: string[],
|
|
578
|
+
options: BatchProcessOptions
|
|
579
|
+
): Promise<Map<string, { success: boolean; outputPath?: string; error?: string }>> {
|
|
580
|
+
const {
|
|
581
|
+
concurrency = 4,
|
|
582
|
+
outputDir,
|
|
583
|
+
transform,
|
|
584
|
+
format = 'jpeg',
|
|
585
|
+
quality = 80,
|
|
586
|
+
} = options;
|
|
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) =>
|
|
594
|
+
queue.add(async () => {
|
|
595
|
+
const filename = path.basename(inputPath, path.extname(inputPath));
|
|
596
|
+
const outputPath = path.join(outputDir, `${filename}.${format}`);
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
let image = sharp(inputPath);
|
|
600
|
+
image = transform(image, filename);
|
|
601
|
+
|
|
602
|
+
await image.toFormat(format, { quality }).toFile(outputPath);
|
|
603
|
+
|
|
604
|
+
results.set(inputPath, { success: true, outputPath });
|
|
605
|
+
} catch (error) {
|
|
606
|
+
results.set(inputPath, {
|
|
607
|
+
success: false,
|
|
608
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
609
|
+
});
|
|
610
|
+
}
|
|
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`;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return { sizes: urls, default: urls[128] };
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
## Best Practices
|
|
718
|
+
|
|
719
|
+
### Do's
|
|
720
|
+
|
|
721
|
+
- **Use WebP/AVIF for web** - Modern formats save 25-50% bandwidth
|
|
722
|
+
- **Implement lazy loading** - Generate LQIP placeholders
|
|
723
|
+
- **Cache processed images** - Store results, don't reprocess
|
|
724
|
+
- **Use streams for large images** - Avoid memory issues
|
|
725
|
+
- **Strip metadata** - Remove EXIF for privacy and size
|
|
726
|
+
- **Validate uploads** - Check dimensions and format
|
|
727
|
+
|
|
728
|
+
### Don'ts
|
|
729
|
+
|
|
730
|
+
- Don't upscale images (use withoutEnlargement)
|
|
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
|
|
736
|
+
|
|
737
|
+
## Related Skills
|
|
738
|
+
|
|
739
|
+
- **media-processing** - Video processing companion
|
|
740
|
+
- **frontend-design** - Image usage in UI
|
|
741
|
+
- **performance-profiling** - Image impact on performance
|
|
742
|
+
|
|
743
|
+
## Reference Resources
|
|
744
|
+
|
|
745
|
+
- [Sharp Documentation](https://sharp.pixelplumbing.com/)
|
|
746
|
+
- [Web.dev Image Optimization](https://web.dev/fast/#optimize-your-images)
|
|
747
|
+
- [Squoosh](https://squoosh.app/) - Format comparison
|
|
748
|
+
- [AVIF Support](https://caniuse.com/avif)
|