$npx -y skills add jezweb/claude-skills --skill image-processingProcess images for web development — resize, crop, trim whitespace, convert formats (PNG/WebP/JPG), optimise file size, generate thumbnails, create OG card images. Uses Pillow (Python) — no ImageMagick needed. Trigger with 'resize image', 'convert to webp', 'trim logo', 'optimise
| 1 | # Image Processing |
| 2 | |
| 3 | Use `img-process` (shipped in `bin/`) for common operations. For complex or custom workflows, generate a Pillow script adapted to the user's environment. |
| 4 | |
| 5 | ## Quick Reference — img-process CLI |
| 6 | |
| 7 | ```bash |
| 8 | img-process resize hero.png --width 1920 |
| 9 | img-process convert logo.png --format webp |
| 10 | img-process trim logo-raw.jpg -o logo-clean.png --padding 10 |
| 11 | img-process thumbnail photo.jpg --size 200 |
| 12 | img-process optimise hero.jpg --quality 85 --max-width 1920 |
| 13 | img-process og-card -o og.png --title "My App" --subtitle "Built for speed" |
| 14 | img-process batch ./images --action convert --format webp -o ./optimised |
| 15 | ``` |
| 16 | |
| 17 | **Use `img-process` when**: the operation is standard (resize, convert, trim, thumbnail, optimise, OG card, batch). This is faster and avoids generating a script each time. |
| 18 | |
| 19 | **Generate a custom script when**: the operation needs logic `img-process` doesn't cover (compositing multiple images, watermarks, complex text layouts, conditional processing). |
| 20 | |
| 21 | ## Prerequisites |
| 22 | |
| 23 | Pillow is required for both `img-process` and custom scripts: |
| 24 | |
| 25 | ```bash |
| 26 | pip install Pillow |
| 27 | ``` |
| 28 | |
| 29 | If Pillow is unavailable, use alternatives: |
| 30 | |
| 31 | | Alternative | Platform | Install | Best for | |
| 32 | |-------------|----------|---------|----------| |
| 33 | | `sips` | macOS (built-in) | None | Resize, convert (no trim/OG) | |
| 34 | | `sharp` | Node.js | `npm install sharp` | Full feature set, high performance | |
| 35 | | `ffmpeg` | Cross-platform | `brew install ffmpeg` | Resize, convert | |
| 36 | |
| 37 | ## Output Format Guide |
| 38 | |
| 39 | | Use case | Format | Why | |
| 40 | |----------|--------|-----| |
| 41 | | Photos, hero images | WebP | Best compression, wide browser support | |
| 42 | | Logos, icons (need transparency) | PNG | Lossless, supports alpha | |
| 43 | | Fallback for older browsers | JPG | Universal support | |
| 44 | | Thumbnails | WebP or JPG | Small file size priority | |
| 45 | | OG cards | PNG | Social platforms handle PNG best | |
| 46 | |
| 47 | ## Core Patterns |
| 48 | |
| 49 | ### Save with Format-Specific Quality |
| 50 | |
| 51 | Different formats need different save parameters. Always handle RGBA-to-JPG compositing — JPG does not support transparency, so composite onto a white background first. |
| 52 | |
| 53 | ```python |
| 54 | from PIL import Image |
| 55 | import os |
| 56 | |
| 57 | def save_image(img, output_path, quality=None): |
| 58 | os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True) |
| 59 | kwargs = {} |
| 60 | ext = output_path.lower().rsplit(".", 1)[-1] |
| 61 | |
| 62 | if ext == "webp": |
| 63 | kwargs = {"quality": quality or 85, "method": 6} |
| 64 | elif ext in ("jpg", "jpeg"): |
| 65 | kwargs = {"quality": quality or 90, "optimize": True} |
| 66 | # RGBA → RGB: composite onto white background |
| 67 | if img.mode == "RGBA": |
| 68 | bg = Image.new("RGB", img.size, (255, 255, 255)) |
| 69 | bg.paste(img, mask=img.split()[3]) |
| 70 | img = bg |
| 71 | elif ext == "png": |
| 72 | kwargs = {"optimize": True} |
| 73 | |
| 74 | img.save(output_path, **kwargs) |
| 75 | ``` |
| 76 | |
| 77 | ### Resize with Aspect Ratio |
| 78 | |
| 79 | When only width or height is given, calculate the other from aspect ratio. Use `Image.LANCZOS` for high-quality downscaling. |
| 80 | |
| 81 | ```python |
| 82 | def resize_image(img, width=None, height=None): |
| 83 | if width and height: |
| 84 | return img.resize((width, height), Image.LANCZOS) |
| 85 | elif width: |
| 86 | ratio = width / img.width |
| 87 | return img.resize((width, int(img.height * ratio)), Image.LANCZOS) |
| 88 | elif height: |
| 89 | ratio = height / img.height |
| 90 | return img.resize((int(img.width * ratio), height), Image.LANCZOS) |
| 91 | return img |
| 92 | ``` |
| 93 | |
| 94 | ### Trim Whitespace (Auto-Crop) |
| 95 | |
| 96 | Remove surrounding whitespace from logos and icons. Convert to RGBA first, then use `getbbox()` to find content bounds. |
| 97 | |
| 98 | ```python |
| 99 | img = Image.open(input_path) |
| 100 | if img.mode != "RGBA": |
| 101 | img = img.convert("RGBA") |
| 102 | bbox = img.getbbox() # Bounding box of non-zero pixels |
| 103 | if bbox: |
| 104 | img = img.crop(bbox) |
| 105 | ``` |
| 106 | |
| 107 | ### Thumbnail |
| 108 | |
| 109 | Fit within max dimensions while maintaining aspect ratio: |
| 110 | |
| 111 | ```python |
| 112 | img.thumbnail((size, size), Image.LANCZOS) |
| 113 | ``` |
| 114 | |
| 115 | ### Optimise for Web |
| 116 | |
| 117 | Resize + compress in one step. Convert to WebP for best compression. Typical settings: width 1920, quality 85. |
| 118 | |
| 119 | ### Cross-Platform Font Discovery |
| 120 | |
| 121 | System font paths differ by OS. Try multiple paths, fall back to Pillow's default. On Linux, `fc-list` can discover fonts dynamically. |
| 122 | |
| 123 | ```python |
| 124 | from PIL import ImageFont |
| 125 | |
| 126 | def get_font(size): |
| 127 | font_paths = [ |
| 128 | # macOS |
| 129 | "/System/Library/Fonts/Helvetica.ttc", |
| 130 | "/System/Library/Fonts/SFNSText.ttf", |
| 131 | # Linux |
| 132 | "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", |
| 133 | "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", |
| 134 | # Windows |
| 135 | "C:/Windows/Fonts/arial.ttf", |
| 136 | ] |
| 137 | for |