$npx -y skills add AlpacaLabsLLC/skills-for-architects --skill product-image-processorDownload, resize, and remove backgrounds from product images at scale. Use when the user asks to "process product images", batch-download images from the schedule, strip backgrounds, or standardize product photos.
| 1 | # /product-image-processor — Product Image Processor |
| 2 | |
| 3 | Download product images from a Google Sheet, normalize sizing, and remove backgrounds. Saves output at each processing stage. |
| 4 | |
| 5 | Works with the **master Google Sheet** — the 33-column schema defined in `../../schema/product-schema.md`. Image URLs are in column AC, product names in column E. Read `../../schema/sheet-conventions.md` for CRUD patterns with MCP tools. |
| 6 | |
| 7 | ## Step 1: Get Input |
| 8 | |
| 9 | If no arguments provided, ask the user: |
| 10 | 1. **Spreadsheet ID** — the Google Sheets ID (from the URL: `docs.google.com/spreadsheets/d/{ID}/...`). 2. **Image URL column** — which column contains image URLs (default: `AC` in the master schema, or the user can specify) |
| 11 | 3. **Name column** (optional) — which column has product names for file naming (default: `E` in the master schema). If not provided, derive names from the image URL/filename. |
| 12 | 4. **Output location** — where to save the images. Suggest `./product-images-YYYY-MM-DD/` as default but let the user pick any path. |
| 13 | 5. **Header row** — whether row 1 is a header (default: yes, row 2 in master schema) |
| 14 | |
| 15 | ## Step 2: Read URLs from Google Sheet |
| 16 | |
| 17 | Use `mcp__google-sheets__list_sheets` to inspect the sheet, then `mcp__google-sheets__get_sheet_data` to read the image URL column and optional name column. |
| 18 | |
| 19 | Build a list of `{ index, url, name }` entries. Skip empty rows. |
| 20 | |
| 21 | ## Step 3: Create Output Folders |
| 22 | |
| 23 | Create the output directory at the user's chosen path with 3 subfolders: |
| 24 | |
| 25 | ``` |
| 26 | <output-path>/ |
| 27 | ├── originals/ # Raw downloads |
| 28 | ├── resized/ # Normalized sizing |
| 29 | └── nobg/ # Background removed |
| 30 | ``` |
| 31 | |
| 32 | If the folder already exists, append a suffix: `-2`, `-3`, etc. |
| 33 | |
| 34 | ## Step 4: Download Images |
| 35 | |
| 36 | Download each image using `curl` in Bash: |
| 37 | |
| 38 | ```bash |
| 39 | curl -L -o "<output-path>" "<url>" |
| 40 | ``` |
| 41 | |
| 42 | **IMPORTANT:** Use `curl`, NOT WebFetch. WebFetch processes content through an AI model which corrupts binary image data. |
| 43 | |
| 44 | Name files as: `001-product-name.png`, `002-product-name.png`, etc. |
| 45 | - Slugify the product name: lowercase, replace spaces/special chars with hyphens, strip consecutive hyphens |
| 46 | - If no name column, extract a name from the URL filename (strip extension and query params) |
| 47 | - If the URL gives no usable name, use `001-image.png`, `002-image.png`, etc. |
| 48 | |
| 49 | If the downloaded file is not a PNG (check extension or content type), convert it to PNG during the resize step. |
| 50 | |
| 51 | ## Step 5: Resize Images |
| 52 | |
| 53 | Run a Python script to resize all images in `originals/` → `resized/`: |
| 54 | |
| 55 | ```python |
| 56 | from PIL import Image |
| 57 | import os, sys |
| 58 | |
| 59 | input_dir = sys.argv[1] # originals/ |
| 60 | output_dir = sys.argv[2] # resized/ |
| 61 | max_edge = int(sys.argv[3]) if len(sys.argv) > 3 else 2000 |
| 62 | |
| 63 | for fname in sorted(os.listdir(input_dir)): |
| 64 | if not fname.lower().endswith(('.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp', '.tiff')): |
| 65 | continue |
| 66 | try: |
| 67 | img = Image.open(os.path.join(input_dir, fname)) |
| 68 | img = img.convert("RGBA") |
| 69 | w, h = img.size |
| 70 | longest = max(w, h) |
| 71 | if longest > max_edge: |
| 72 | scale = max_edge / longest |
| 73 | new_w, new_h = int(w * scale), int(h * scale) |
| 74 | img = img.resize((new_w, new_h), Image.LANCZOS) |
| 75 | out_name = os.path.splitext(fname)[0] + ".png" |
| 76 | img.save(os.path.join(output_dir, out_name), "PNG") |
| 77 | print(f"OK: {fname} → {out_name} ({img.size[0]}x{img.size[1]})") |
| 78 | except Exception as e: |
| 79 | print(f"FAIL: {fname} — {e}") |
| 80 | ``` |
| 81 | |
| 82 | Rules: |
| 83 | - Max **2000px** on the longest edge (configurable if user requests) |
| 84 | - Preserve aspect ratio |
| 85 | - Do NOT upscale — if already smaller than max, keep original dimensions |
| 86 | - Convert everything to PNG (RGBA mode for transparency support) |
| 87 | |
| 88 | ## Step 6: Remove Backgrounds |
| 89 | |
| 90 | Check if `rembg` is installed. If not, install it: |
| 91 | |
| 92 | ```bash |
| 93 | pip3 install rembg onnxruntime |
| 94 | ``` |
| 95 | |
| 96 | Then run background removal on all resized images → `nobg/`: |
| 97 | |
| 98 | ```python |
| 99 | from rembg import remove |
| 100 | from PIL import Image |
| 101 | import os, sys, io |
| 102 | |
| 103 | input_dir = sys.argv[1] # resized/ |
| 104 | output_dir = sys.argv[2] # nobg/ |
| 105 | |
| 106 | for fname in sorted(os.listdir(input_dir)): |
| 107 | if not fname.lower().endswith('.png'): |
| 108 | continue |
| 109 | try: |
| 110 | input_path = os.path.join(input_dir, fname) |
| 111 | with open(input_path, 'rb') as f: |
| 112 | input_data = f.read() |
| 113 | output_data = remove(input_data) |
| 114 | img = Image.open(io.BytesIO(output_data)) |
| 115 | img.save(os.path.join(output_dir, fname), "PNG") |
| 116 | print(f"OK: {fname}") |
| 117 | except Exception as e: |
| 118 | print(f"FAIL: {fname} — {e}") |
| 119 | ``` |
| 120 | |
| 121 | **Note:** The first run of rembg downloads the u2net model (~170MB). Warn the user |