Product Photo Maker Sub-Package
Product Photo Maker Sub-Package
CLI tool for generating standardized 1600x1600 product photos with ML-based background removal, fabric texture background compositing, and programmatic shadow generation.
Purpose
Produces consistent, marketplace-ready product photos from raw source images. Used before the main image pipeline (image-processor) to prepare product photos for the website and Mercari listings.
Source images from manufacturers typically come with white backgrounds. This tool removes the background, composites the product onto the shop’s fabric texture, and adds realistic shadows — all locally with no API costs.
Before / After
Source image from manufacturer (white background):
After processing with --shadow --float (background removed, composited on fabric texture, floating shadow added):
Sub-Package Structure
sub-packages/product-photo-maker/
├── package.json
├── CLAUDE.md
├── assets/
│ └── default-bg.jpg # Bundled fabric texture background (1.7MB)
├── bin/
│ └── make-product-photo.mjs # CLI entry point
└── src/
├── make-product-photo.mjs # Core processing logic
└── shadow.mjs # Programmatic shadow generation
Processing Pipeline
graph TB
A[Source Image] --> B{--no-bg-removal?}
B -->|Yes| D[Read file as-is]
B -->|No| C[ML Background Removal<br/>@imgly/background-removal-node]
C --> E{--no-trim?}
E -->|Yes| F[Keep as-is]
E -->|No| G[Trim transparent pixels]
D --> H[Resize to 1440x1440 container<br/>fit: contain]
F --> H
G --> H
H --> I{--shadow?}
I -->|No| J[Composite onto background]
I -->|Yes| K[Generate shadow layers]
K --> L[Composite: bg → shadows → product]
J --> M[Output JPEG<br/>quality: 90]
L --> M
Output Specifications
| Property | Value |
|---|---|
| Canvas size | 1600x1600 px |
| Container size | 1440x1440 px (90% of canvas) |
| Container offset | 80px from each edge |
| Output format | JPEG, quality 90 |
| Default background | Fabric texture (assets/default-bg.jpg) |
| Product fitting | object-fit: contain behavior |
Shadow System
The shadow system generates multiple layers that composite together for a realistic result. All shadow generation is 100% programmatic using sharp — no API calls, no ML models, zero cost.
Why Shadows Matter
Without shadows, products composited onto the texture look like they’re “floating” — a flat cutout pasted on a background:
With shadows, the product feels like it’s physically present on the surface:
Shadow Modes
Two shadow modes for different product types:
Floating (--shadow --float)
For Eurorack modular synth panels. The panel appears to hover above the surface with a projected shadow beneath it.
Shadow layers (5 total):
- Vignette — asymmetric darkening from upper-left light source
- Projected shadow — product alpha squashed to 25% height, placed below product
- Bottom-only wide shadow — blur 120, gradient-masked, large offset
- Bottom-only medium shadow — blur 52, gradient-masked, medium offset
- Contact shadow — tight, sharp shadow at product edge
Grounded (--shadow)
For desktop products (MIDI controllers, standalone units). The product sits on the surface with a tight contact shadow.
Shadow layers (4 total, no projected shadow):
- Vignette — same asymmetric darkening
- Bottom-only wide shadow — blur 100, small offset
- Bottom-only medium shadow — blur 40, small offset
- Contact shadow — tight shadow directly beneath product
Shadow Implementation Details
All shadow layers are generated from the product’s alpha channel:
// Extract alpha as raw single-channel buffer
const alphaRaw = await sharp(productPngBuffer)
.extractChannel(3)
.raw()
.toBuffer();
// Each shadow layer: offset alpha → blur → scale opacity → black RGBA
const blurredAlpha = await sharp(offsetAlpha, {
raw: { width: 1600, height: 1600, channels: 1 },
})
.blur(blurRadius)
.linear(opacity, 0)
.png()
.toBuffer();
Key techniques:
- Alpha extraction:
extractChannel(3)gets the product silhouette - Projected shadow: Alpha cropped to bounding box, squashed vertically with
resize(bw, newH, { fit: 'fill' }), placed below product - Gradient masking: Bottom-only shadows use a per-pixel gradient mask that fades from 0 (top of product) to 1 (below product)
- Vignette: Per-pixel distance calculation from light source position, using
(distSq / spreadSq) ** 0.75for natural falloff - Black RGBA layers: Each shadow is a black canvas with the blurred alpha as transparency, composited with
overblend mode
CLI Usage
node ./sub-packages/product-photo-maker/bin/make-product-photo.mjs <input> [...more] [options]
Options
| Option | Short | Description |
|---|---|---|
--output <dir> | -o | Output directory (default: __inbox/prod-imgs-exported/) |
--with-bg <path> | Custom background image instead of default fabric texture | |
--no-bg-removal | Skip ML background removal (use image as-is) | |
--no-trim | Skip trimming after background removal | |
--shadow | Add directional shadow (grounded, upper-left light) | |
--float | Use floating shadow for modular synth panels (implies --shadow) | |
--help | -h | Show help message |
Examples
# Basic: bg removal + trim + fabric texture (no shadow)
node ./sub-packages/product-photo-maker/bin/make-product-photo.mjs ./imgs/product.jpg
# Eurorack module with floating shadow
node ./sub-packages/product-photo-maker/bin/make-product-photo.mjs panel.jpg --shadow --float
# Desktop product with grounded shadow
node ./sub-packages/product-photo-maker/bin/make-product-photo.mjs controller.jpg --shadow
# Edge-to-edge product photo (skip bg removal) with grounded shadow
node ./sub-packages/product-photo-maker/bin/make-product-photo.mjs photo.jpg --no-bg-removal --shadow
# Multiple images
node ./sub-packages/product-photo-maker/bin/make-product-photo.mjs img1.jpg img2.png --shadow --float
# Custom output directory
node ./sub-packages/product-photo-maker/bin/make-product-photo.mjs -o ./my-output product.jpg --shadow
Flag Selection Guide
Choosing the right flags depends on the source image characteristics:
Background Removal Decision
- Product fills entire frame, no background visible →
--no-bg-removal
- Example: product-only photo taken on a solid backdrop that fills edge-to-edge
- Avoids ML model damaging product colors (especially dark/black products)
- Fastest mode (~0.2s per image)
- Product on a background, product fills most of the frame →
--no-trim
- Example: studio shot with small margins
- Background removal runs but trimming is skipped to prevent overcropping
- Product on a background with significant space around it → default (no flags)
- Example: product on white/gray background with padding
- Full pipeline: bg removal → trim → resize → composite
- ~1.5-2.5s per image (ML model processing)
Shadow Decision
Always add --shadow for product photos. Then decide float vs grounded:
- Eurorack modular synth panels →
--shadow --float- Tall-narrow rectangles (3 to 5 aspect ratio)
- Visible metal/aluminum faceplate, rows of 3.5mm jacks
- Panel hovers above surface in real use
- Desktop/standalone products →
--shadow(grounded)- MIDI controllers, desktop units, boxes
- Products that sit flat on a surface
The /l-make-product-photo Claude Code skill auto-detects the product type and selects the appropriate flags. It examines the image to determine whether background removal is needed, whether the product is a modular panel or desktop unit, and groups images by flag combination for batch processing.
Background Removal Notes
The ML-based background removal (@imgly/background-removal-node) works well for most images but has a known limitation: it can create “holes” in the product for dark/black products where the model confuses product interior with background. This is especially common with dark panels that have internal dark areas. The workaround is to use --no-bg-removal for edge-to-edge product photos where no background removal is needed.
Dependencies
| Package | Purpose |
|---|---|
@imgly/background-removal-node | ML-based background removal (runs locally, no API key) |
sharp | Image resizing, compositing, shadow generation, and format conversion |
chalk | Colored CLI output |
The first run downloads the ML model (~30MB). Subsequent runs reuse the cached model and are faster.
API Reference
makeProductPhoto(inputPath, outputDir, options)
Core exported function for programmatic use.
Parameters:
inputPath(string): Path to the source imageoutputDir(string): Output directory path (created if missing)options(object):noTrim(boolean, defaultfalse): Skip transparent pixel trimmingnoBgRemoval(boolean, defaultfalse): Skip ML background removalwithBg(string | null, defaultnull): Custom background image pathshadow(boolean, defaultfalse): Add directional shadowfloat(boolean, defaultfalse): Use floating shadow (for Eurorack panels)
Returns: Promise<string> — path to the output JPEG file
Output filename: Same as input with .jpg extension (e.g., product.heic → product.jpg)
generateShadowLayers(productPngBuffer, options)
Generates all shadow layers for a product image. Exported from src/shadow.mjs.
Parameters:
productPngBuffer(Buffer): Full-canvas (1600x1600) transparent PNG with productoptions(object):float(boolean, defaultfalse): Use floating shadow mode
Returns: Promise<Buffer[]> — array of RGBA PNG buffers to composite in order
Workflow Integration
This tool sits before the main image pipeline:
graph LR
A[Raw Photo] -->|product-photo-maker| B[Standardized 1600x1600 JPEG]
B -->|Copy to /imgs/| C[Image Source Directory]
C -->|pnpm convimgs| D[WebP variants + OGP + Mercari PNG]
D -->|pnpm r2:upload| E[Cloudflare R2 CDN]
Typical workflow:
- Run
make-product-photoon source images - Copy output to
/imgs/with appropriate naming (add__ogsuffix for OGP) - Run
pnpm convimgsto generate web variants - Run
pnpm r2:uploadto deploy to CDN - Reference image slug in product data or MDX frontmatter
Relationship to Image Processor
| Aspect | Product Photo Maker | Image Processor |
|---|---|---|
| Purpose | Prepare raw photos into standardized product images | Generate web-optimized variants for the site |
| Input | Raw camera photos (any format) | Standardized images in /imgs/ |
| Output | Single 1600x1600 JPEG with shadow | WebP variants, OGP, Mercari PNG, metadata |
| Pipeline position | Before /imgs/ | After /imgs/ |
| Key dependency | @imgly/background-removal-node, sharp | sharp (+ blurhash) |