$npx -y skills add AlpacaLabsLLC/skills-for-architects --skill resize-imagesBatch-resize images for web (WebP at 1920/1200/400px), social (center-cropped for Instagram, Twitter/X, LinkedIn), slides (JPEG at 1024×768 and 1920×1080), and print (300 DPI JPEG at ARCH A/B/C). Use when the user asks to "resize images", prepare images for web, social, slides, o
| 1 | # /resize-images — Image Resizer for Web, Social, and Print |
| 2 | |
| 3 | Resize project photos and renders for web publishing, social media, and print layouts. Always asks the user for the source folder before doing anything. Outputs resized copies into clearly named subfolders — originals are never modified. |
| 4 | |
| 5 | ## Step 1: Ask for the source folder |
| 6 | |
| 7 | Before doing anything else, ask: |
| 8 | |
| 9 | > "Which folder contains the images you'd like to resize?" |
| 10 | |
| 11 | Wait for the user's response. Accept any valid path — absolute, relative, or with `~`. Expand `~` to the user's home directory. |
| 12 | |
| 13 | Then ask: |
| 14 | |
| 15 | > "Which outputs do you need? (choose one or more) |
| 16 | > - **Web** — WebP at 1920px (hero), 1200px (standard), 400px (thumb) |
| 17 | > - **Social** — center-cropped WebP: Instagram square (1080×1080), Instagram portrait (1080×1350), Twitter/X (1200×675), LinkedIn (1200×627) |
| 18 | > - **Slides** — center-cropped JPEG: standard 4:3 (1024×768), widescreen 16:9 (1920×1080) |
| 19 | > - **Print** — 300 DPI JPEG at ARCH A (9×12), ARCH B (12×18), ARCH C (18×24) |
| 20 | > - **All**" |
| 21 | |
| 22 | ## Step 2: Scan the folder |
| 23 | |
| 24 | List all image files in the folder (non-recursive). Supported formats: `.jpg`, `.jpeg`, `.png`, `.tif`, `.tiff`, `.webp`. |
| 25 | |
| 26 | ```python |
| 27 | import os, sys |
| 28 | |
| 29 | folder = sys.argv[1] |
| 30 | exts = {'.jpg', '.jpeg', '.png', '.tif', '.tiff', '.webp'} |
| 31 | images = [f for f in os.listdir(folder) if os.path.splitext(f.lower())[1] in exts] |
| 32 | images.sort() |
| 33 | for img in images: |
| 34 | print(img) |
| 35 | ``` |
| 36 | |
| 37 | Report the count: `"Found N image(s) in [folder]. Ready to resize."` |
| 38 | |
| 39 | If the folder is empty or contains no supported images, tell the user and stop. |
| 40 | |
| 41 | ## Step 3: Check for Pillow |
| 42 | |
| 43 | Run this to confirm Pillow is available: |
| 44 | |
| 45 | ```bash |
| 46 | python3 -c "import PIL; print('ok')" 2>/dev/null || echo "missing" |
| 47 | ``` |
| 48 | |
| 49 | If missing, tell the user: |
| 50 | |
| 51 | > "Pillow isn't installed. Run `pip install Pillow` then try again." |
| 52 | |
| 53 | Stop if Pillow is unavailable. |
| 54 | |
| 55 | ## Step 4: Create output folders |
| 56 | |
| 57 | Inside the source folder, create one subfolder per requested mode: |
| 58 | |
| 59 | - `resized-web/` — if web sizes were requested |
| 60 | - `resized-social/` — if social sizes were requested |
| 61 | - `resized-slides/` — if slides sizes were requested |
| 62 | - `resized-print/` — if print sizes were requested |
| 63 | |
| 64 | ```python |
| 65 | import os, sys |
| 66 | folder = sys.argv[1] |
| 67 | modes = sys.argv[2:] # 'web', 'social', 'slides', and/or 'print' |
| 68 | for mode in modes: |
| 69 | os.makedirs(os.path.join(folder, f"resized-{mode}"), exist_ok=True) |
| 70 | ``` |
| 71 | |
| 72 | ## Step 5: Resize images |
| 73 | |
| 74 | Run this Python script via Bash, passing the folder path and modes as arguments: |
| 75 | |
| 76 | ```python |
| 77 | import sys |
| 78 | import os |
| 79 | from PIL import Image |
| 80 | |
| 81 | folder = sys.argv[1] |
| 82 | modes = sys.argv[2:] # 'web', 'social', and/or 'print' |
| 83 | |
| 84 | exts = {'.jpg', '.jpeg', '.png', '.tif', '.tiff', '.webp'} |
| 85 | images = [f for f in os.listdir(folder) if os.path.splitext(f.lower())[1] in exts] |
| 86 | |
| 87 | WEB_SIZES = [ |
| 88 | ("hero", 1920), |
| 89 | ("standard", 1200), |
| 90 | ("thumb", 400), |
| 91 | ] |
| 92 | |
| 93 | # Social: exact crop dimensions (width, height) |
| 94 | SOCIAL_SIZES = [ |
| 95 | ("social-square", (1080, 1080)), |
| 96 | ("social-portrait", (1080, 1350)), |
| 97 | ("social-landscape", (1200, 675)), |
| 98 | ("social-linkedin", (1200, 627)), |
| 99 | ] |
| 100 | |
| 101 | # Slides: center-crop to fill slide canvas |
| 102 | SLIDES_SIZES = [ |
| 103 | ("slides-standard", (1024, 768)), # 4:3 |
| 104 | ("slides-wide", (1920, 1080)), # 16:9 |
| 105 | ] |
| 106 | |
| 107 | # ARCH sizes in pixels at 300 DPI: inches × 300 |
| 108 | PRINT_SIZES = [ |
| 109 | ("arch-a", (9 * 300, 12 * 300)), # 2700 × 3600 |
| 110 | ("arch-b", (12 * 300, 18 * 300)), # 3600 × 5400 |
| 111 | ("arch-c", (18 * 300, 24 * 300)), # 5400 × 7200 |
| 112 | ] |
| 113 | |
| 114 | def center_crop(img, target_w, target_h): |
| 115 | """Scale to fill target dimensions, then center-crop.""" |
| 116 | src_w, src_h = img.size |
| 117 | scale = max(target_w / src_w, target_h / src_h) |
| 118 | scaled_w = int(src_w * scale) |
| 119 | scaled_h = int(src_h * scale) |
| 120 | img = img.resize((scaled_w, scaled_h), Image.LANCZOS) |
| 121 | left = (scaled_w - target_w) // 2 |
| 122 | top = (scaled_h - target_h) // 2 |
| 123 | return img.crop((left, top, left + target_w, top + target_h)) |
| 124 | |
| 125 | for filename in images: |
| 126 | stem = os.path.splitext(filename)[0] |
| 127 | src_path = os.path.join(folder, filename) |
| 128 | |
| 129 | try: |
| 130 | img = Image.open(src_path) |
| 131 | # Convert to RGB if needed (handles RGBA, palette, etc.) |
| 132 | if img.mode not in ("RGB", "L"): |
| 133 | img = img.convert("RGB") |
| 134 | |
| 135 | print(f"Processing: {filename} ({img.width}×{img.height})") |
| 136 | |
| 137 | if "web" in modes: |
| 138 | web_dir = os.path.join(folder, "resized-web") |
| 139 | for label, max_width in WEB_SIZES: |
| 140 | out_img = img.copy() |
| 141 | if out_img.width > max_width: |