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.
Files changed (27) hide show
  1. package/package.json +2 -2
  2. package/plugin/skills/backend/api-architecture/SKILL.md +857 -0
  3. package/plugin/skills/backend/caching-strategies/SKILL.md +755 -0
  4. package/plugin/skills/backend/event-driven-architecture/SKILL.md +753 -0
  5. package/plugin/skills/backend/real-time-systems/SKILL.md +635 -0
  6. package/plugin/skills/databases/database-optimization/SKILL.md +571 -0
  7. package/plugin/skills/databases/postgresql/SKILL.md +494 -18
  8. package/plugin/skills/devops/docker/SKILL.md +466 -18
  9. package/plugin/skills/devops/monorepo-management/SKILL.md +595 -0
  10. package/plugin/skills/devops/observability/SKILL.md +622 -0
  11. package/plugin/skills/devops/performance-profiling/SKILL.md +905 -0
  12. package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
  13. package/plugin/skills/frameworks/react/SKILL.md +1006 -32
  14. package/plugin/skills/frontend/advanced-ui-design/SKILL.md +426 -0
  15. package/plugin/skills/integrations/ai-integration/SKILL.md +730 -0
  16. package/plugin/skills/integrations/payment-integration/SKILL.md +735 -0
  17. package/plugin/skills/languages/python/SKILL.md +489 -25
  18. package/plugin/skills/languages/typescript/SKILL.md +379 -30
  19. package/plugin/skills/methodology/problem-solving/SKILL.md +355 -0
  20. package/plugin/skills/methodology/research-validation/SKILL.md +668 -0
  21. package/plugin/skills/methodology/sequential-thinking/SKILL.md +260 -0
  22. package/plugin/skills/mobile/mobile-development/SKILL.md +756 -0
  23. package/plugin/skills/security/security-hardening/SKILL.md +633 -0
  24. package/plugin/skills/tools/document-processing/SKILL.md +916 -0
  25. package/plugin/skills/tools/image-processing/SKILL.md +748 -0
  26. package/plugin/skills/tools/mcp-development/SKILL.md +883 -0
  27. 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)