omgkit 2.2.0 → 2.3.0

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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/plugin/skills/databases/mongodb/SKILL.md +60 -776
  3. package/plugin/skills/databases/prisma/SKILL.md +53 -744
  4. package/plugin/skills/databases/redis/SKILL.md +53 -860
  5. package/plugin/skills/devops/aws/SKILL.md +68 -672
  6. package/plugin/skills/devops/github-actions/SKILL.md +54 -657
  7. package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
  8. package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
  9. package/plugin/skills/frameworks/django/SKILL.md +87 -853
  10. package/plugin/skills/frameworks/express/SKILL.md +95 -1301
  11. package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
  12. package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
  13. package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
  14. package/plugin/skills/frameworks/react/SKILL.md +94 -962
  15. package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
  16. package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
  17. package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
  18. package/plugin/skills/frontend/responsive/SKILL.md +76 -799
  19. package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
  20. package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
  21. package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
  22. package/plugin/skills/languages/javascript/SKILL.md +106 -849
  23. package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
  24. package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
  25. package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
  26. package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
  27. package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
  28. package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
  29. package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
  30. package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
  31. package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
  32. package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
  33. package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
  34. package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
  35. package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
  36. package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
  37. package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
  38. package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
  39. package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
  40. package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
  41. package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
  42. package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
  43. package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
  44. package/plugin/skills/security/better-auth/SKILL.md +46 -1034
  45. package/plugin/skills/security/oauth/SKILL.md +80 -934
  46. package/plugin/skills/security/owasp/SKILL.md +78 -862
  47. package/plugin/skills/testing/playwright/SKILL.md +77 -700
  48. package/plugin/skills/testing/pytest/SKILL.md +73 -811
  49. package/plugin/skills/testing/vitest/SKILL.md +60 -920
  50. package/plugin/skills/tools/document-processing/SKILL.md +111 -838
  51. package/plugin/skills/tools/image-processing/SKILL.md +126 -659
  52. package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
  53. package/plugin/skills/tools/media-processing/SKILL.md +118 -735
  54. package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
  55. package/plugin/skills/SKILL_STANDARDS.md +0 -743
@@ -1,6 +1,6 @@
1
1
  ---
2
- name: image-processing
3
- description: Enterprise image manipulation with Sharp including optimization, resizing, format conversion, and batch operations
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
- # Image Processing
15
+ # Processing Images
16
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
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
- // 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
- })
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
- // 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 } })
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
- // 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);
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
- ### 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>();
49
+ ## Features
181
50
 
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
- );
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
- return results;
191
- }
64
+ ## Common Patterns
192
65
 
193
- // Generate srcset for responsive images
194
- interface SrcsetOptions {
195
- widths: number[];
196
- formats: ('jpeg' | 'webp' | 'avif')[];
197
- quality?: number;
198
- }
66
+ ### Responsive Image Set Generation
199
67
 
200
- async function generateSrcset(
68
+ ```typescript
69
+ async function generateResponsiveSet(
201
70
  inputPath: string,
202
71
  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;
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 sources: Array<{ srcset: string; type: string }> = [];
211
-
212
- for (const format of formats) {
213
- const srcsetParts: string[] = [];
75
+ const srcsetParts: string[] = [];
214
76
 
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
- });
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
- sources,
242
- fallback: `${baseName}-fallback.jpg`,
87
+ srcset: srcsetParts.join(', '),
88
+ sizes: '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw',
243
89
  };
244
90
  }
245
91
  ```
246
92
 
247
- ### 3. Image Effects & Filters
93
+ ### E-commerce Product Image Processing
248
94
 
249
95
  ```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
- }
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
- // 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
- }
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
- // 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
- }
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
- // 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();
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
- ### 4. Watermarks & Overlays
125
+ ### Image Watermarking
341
126
 
342
127
  ```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
-
128
+ async function addWatermark(inputPath: string, outputPath: string, watermarkPath: string): Promise<void> {
362
129
  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)
130
+ const watermark = await sharp(watermarkPath)
131
+ .resize(Math.round((metadata.width || 800) * 0.2))
423
132
  .toBuffer();
424
133
 
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 })
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
- // Get comprehensive image info
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 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
- }
141
+ const { width = 800, height = 600 } = metadata;
534
142
 
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);
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
- return {
555
- sharpness: laplacianVariance,
556
- brightness: mean / 255,
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
- ### 6. Batch Processing
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
- 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) =>
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, filename);
601
-
602
- await image.toFormat(format, { quality }).toFile(outputPath);
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
- return { sizes: urls, default: urls[128] };
185
+ await queue.onIdle();
186
+ return results;
714
187
  }
715
188
  ```
716
189
 
717
190
  ## Best Practices
718
191
 
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
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 companion
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
- ## Reference Resources
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 Support](https://caniuse.com/avif)
214
+ - [Squoosh](https://squoosh.app/) - Format comparison tool
215
+ - [Can I Use AVIF](https://caniuse.com/avif)