Convert, inspect, and batch-process images using the img-convert CLI, Node.js API, or MCP server. Covers format conversion (JPEG, PNG, WebP, AVIF, GIF, TIFF), metadata, resize, rotate, grayscale, blur, normalize, trim, URL input, manifest batch mode, and JSON output for agent pipelines.
Install
npx skillscat add dutchbase/img-converter Install via the SkillsCat registry.
img-convert Skill
img-convert is a Sharp-based image conversion tool available as a CLI, Node.js API, and MCP server. This skill covers how to use each interface correctly, choose between them, and build reliable agent workflows.
Interface Selection
Choose the right interface for the context:
| Situation | Use |
|---|---|
| Shell task, build script, CI pipeline | CLI (img-convert) |
| Node.js code that needs the output buffer | Node.js API (import { convert } from '@dutchbase/img-convert') |
| Claude Code or MCP-enabled AI agent | MCP tools (convert_image, get_image_info) |
| Browser or external HTTP client | REST API (POST /api/convert) |
| Multiple files with different settings each | CLI batch subcommand or API batch() |
Inspect Before Converting
Always run info first on unknown images. It reveals format, dimensions, alpha, EXIF, animation, and color space — all of which affect conversion decisions.
img-convert info photo.jpg{
"format": "jpeg",
"width": 4032,
"height": 3024,
"filesize": 3891200,
"hasAlpha": false,
"hasExif": true,
"colorSpace": "srgb",
"isAnimated": false,
"channels": 3,
"density": 72
}Key decisions driven by info:
hasAlpha: true+ converting to JPEG → must pass--background "#ffffff"or pixels go blackisAnimated: true→ only GIF→WebP preserves animation; all other targets capture frame 1 onlycolorSpace: "cmyk"→ convert tosrgbfirst for web usewidth × height > 25_000_000→ tool will reject withIMAGE_TOO_LARGE; resize first
CLI Reference
Convert (default command)
img-convert [files...] -f <format> [options]files accepts paths, glob patterns, and HTTP/HTTPS URLs.
All flags
| Flag | Default | Description |
|---|---|---|
-f, --format |
— | Required. jpeg png webp avif gif tiff |
-q, --quality <n> |
85 |
1–100. JPEG/WebP/AVIF/TIFF. PNG uses derived compression. GIF ignores it. |
--width <n> |
— | Resize width in pixels. Maintains aspect ratio. |
--height <n> |
— | Resize height in pixels. Maintains aspect ratio. |
--no-metadata |
— | Strip EXIF/XMP/IPTC. ICC profile always kept. |
-o, --output <dir> |
input dir | Output directory. Created if absent. |
-c, --concurrency <n> |
4 |
Parallel workers. |
--json |
— | Data → stdout as JSON. Progress/errors → stderr. |
--dry-run |
— | Preview without writing. |
--quiet |
— | Suppress per-file lines. |
--grayscale |
— | Desaturate to greyscale. |
--rotate <n> |
— | Rotate by degrees. Empty corners filled with --background. |
--flip |
— | Horizontal mirror (left–right). |
--flop |
— | Vertical mirror (top–bottom). |
--background <color> |
— | Fill color: #ffffff, rgba(0,0,0,0), etc. |
--blur <sigma> |
— | Gaussian blur, sigma 0.3–1000. |
--sharpen |
— | Unsharp mask with default parameters. |
--normalize |
— | Stretch contrast to full range. Good for scans. |
--trim |
— | Remove uniform-color border pixels. |
Common patterns
# Single file
img-convert photo.jpg -f webp
# Machine-readable output — stdout = JSON, stderr = progress
img-convert photo.jpg -f webp --json 2>/dev/null
# Batch glob with output dir
img-convert "src/**/*.png" -f avif -q 80 -o dist/images/
# Resize to max 1280px wide
img-convert banner.png -f jpeg --width 1280
# PNG with transparency → JPEG (flatten to white)
img-convert logo.png -f jpeg --background "#ffffff"
# Strip metadata, web-optimised
img-convert photo.jpg -f webp --no-metadata -q 85
# Grayscale scanned document with auto contrast
img-convert scan.jpg -f png --grayscale --normalize
# Remote URL
img-convert https://example.com/photo.png -f webp -o ./converted/
# Pipe mode (stdin → stdout)
cat input.png | img-convert -f webp > output.webp
# Dry run — preview without writing
img-convert "*.jpg" -f avif --dry-run --json 2>/dev/nullJSON output shapes
Single file (--json):
{
"input": "photo.jpg",
"output": "/abs/path/photo.webp",
"inputBytes": 204800,
"outputBytes": 81920,
"reduction": 60.0,
"width": 1920,
"height": 1080,
"format": "webp",
"quality": 85
}Multiple files: JSON array of the above. Failed items have "error": "<message>" instead of size fields.
info subcommand
img-convert info <file|url>Always outputs JSON to stdout. No flags needed.
# Check before converting
img-convert info logo.png | jq '{hasAlpha, width, height}'batch subcommand
Convert files defined in a JSON manifest. The agent writes the manifest, batch executes it.
img-convert batch <manifest.json> [--json] [-c <n>]Manifest format:
[
{ "input": "hero.png", "output": "hero.webp", "format": "webp", "quality": 90 },
{ "input": "thumb.jpg", "output": "thumb.avif", "format": "avif", "width": 200 },
{ "input": "https://cdn.example.com/bg.png", "format": "jpeg", "removeMetadata": true }
]Manifest fields: input (required), format (required), output, quality, width, height, removeMetadata.
# Execute manifest, capture JSON results
img-convert batch jobs.json --json > results.json 2>/dev/nullJSON result per item:
{
"index": 0,
"input": "hero.png",
"output": "hero.webp",
"inputBytes": 512000,
"outputBytes": 102400,
"reduction": 80.0,
"width": 1920,
"height": 1080,
"format": "webp",
"quality": 90
}MCP Tools (Claude Code / MCP Agents)
When the MCP server is registered, use these tools directly without shell commands.
get_image_info
get_image_info({ input_path: "photo.jpg" })
→ { format, width, height, filesize, hasAlpha, hasExif, colorSpace, isAnimated, channels, density }convert_image
convert_image({
input_path: "photo.jpg", // path or URL — required
output_format: "webp", // required
output_path: "photo.webp", // optional, auto-derived if omitted
quality: 85,
width: 1280,
height: 720,
remove_metadata: false,
grayscale: false,
rotate: 0,
background: "#ffffff"
})
→ { input_path, output_path, input_bytes, output_bytes, reduction, width, height, format, quality }batch_convert
batch_convert({
items: [
{ input_path: "a.jpg", output_format: "webp" },
{ input_path: "b.png", output_format: "avif", width: 400 }
],
concurrency: 4
})
→ Array of result objectslist_supported_formats
list_supported_formats()
→ { input: ["jpeg","png","webp","avif","gif","tiff","heic","svg","bmp"], output: ["jpeg","png","webp","avif","gif","tiff"] }Register the MCP server
Add to ~/.claude/mcp.json (or equivalent for your client):
{
"mcpServers": {
"img-convert": {
"command": "img-convert",
"args": ["mcp"]
}
}
}Node.js API
import { convert, getInfo, batch } from '@dutchbase/img-convert'All functions accept file paths, HTTP/HTTPS URLs, or Buffer.
convert(input, options) → Promise<{ buffer, info }>
const result = await convert('./photo.jpg', {
format: 'webp',
quality: 85,
width: 1280,
background: '#ffffff', // required for PNG→JPEG with transparency
removeMetadata: true,
autoRotate: true, // apply EXIF orientation and strip tag
})
// result.buffer — converted image Buffer
// result.info — { inputBytes, outputBytes, width, height, format }Full options:
interface ConvertApiOptions {
format: 'jpeg'|'png'|'webp'|'avif'|'gif'|'tiff' // required
quality?: number // default 85
width?: number
height?: number
removeMetadata?: boolean // default false
maintainAspectRatio?: boolean // default true
allowUpscaling?: boolean // default false
crop?: { left: number; top: number; width: number; height: number }
rotate?: number // degrees
autoRotate?: boolean // use EXIF orientation tag
flip?: boolean // horizontal mirror
flop?: boolean // vertical mirror
background?: string // CSS color string
grayscale?: boolean
blur?: number // Gaussian sigma 0.3–1000
sharpen?: boolean
normalize?: boolean
trim?: boolean
}getInfo(input) → Promise<ImageInfo>
const info = await getInfo('./photo.jpg')
// { format, width, height, filesize, hasAlpha, hasExif, colorSpace, isAnimated, channels, density }
// Pattern: inspect then decide
const { hasAlpha, isAnimated } = await getInfo(inputPath)
if (isAnimated) throw new Error('Animated images not supported in this pipeline')
const result = await convert(inputPath, {
format: 'jpeg',
...(hasAlpha && { background: '#ffffff' }),
})batch(items, options) → Promise<BatchApiResult[]>
const results = await batch([
{ input: './hero.png', format: 'webp', quality: 90 },
{ input: './thumb.jpg', format: 'avif', width: 200 },
], { concurrency: 4 })Format Decision Guide
| Goal | Recommended format | Notes |
|---|---|---|
| Web photo | webp |
Best size/quality for most images. Supported in all modern browsers. |
| Web photo, maximum compression | avif |
20–30% smaller than WebP. Slower to encode. |
| Web photo, maximum compatibility | jpeg |
JPEG 2024 still has near-universal support. |
| Transparency for web | webp or png |
WebP smaller; PNG for lossless + alpha. |
| Print / archival | tiff |
Lossless or high-quality. Large files. |
| Lossless screenshot / icon | png |
|
| Animation | gif (keep) or webp (convert) |
WebP animation is smaller than GIF. |
| HEIC from iPhone | any output format | heic is input-only. Convert to jpeg or webp. |
Agent Workflow Patterns
Pattern 1: Inspect → decide → convert
# Step 1: inspect
INFO=$(img-convert info ./photo.png)
HAS_ALPHA=$(echo "$INFO" | jq .hasAlpha)
IS_ANIMATED=$(echo "$INFO" | jq .isAnimated)
# Step 2: decide
if [ "$IS_ANIMATED" = "true" ]; then
FORMAT="gif"
EXTRA_FLAGS=""
elif [ "$HAS_ALPHA" = "true" ]; then
FORMAT="webp"
EXTRA_FLAGS=""
else
FORMAT="jpeg"
EXTRA_FLAGS='--background "#ffffff"'
fi
# Step 3: convert with JSON output
img-convert ./photo.png -f "$FORMAT" $EXTRA_FLAGS --json 2>/dev/nullPattern 2: Agent generates manifest → CLI executes
// Agent assembles jobs as structured data — no shell interpolation
const manifest = imagePaths.map(inputPath => ({
input: inputPath,
output: inputPath.replace(/\.\w+$/, '.webp'),
format: 'webp' as const,
quality: 85,
removeMetadata: true,
}))
fs.writeFileSync('jobs.json', JSON.stringify(manifest, null, 2))
const stdout = execSync('img-convert batch jobs.json --json 2>/dev/null', { encoding: 'utf8' })
const results = JSON.parse(stdout) as BatchResult[]
const saved = results.reduce((n, r) => n + (r.inputBytes - r.outputBytes), 0)
console.log(`Saved ${(saved / 1024 / 1024).toFixed(1)} MB`)Pattern 3: Filter failed jobs and retry
RESULTS=$(img-convert batch jobs.json --json 2>/dev/null)
FAILED=$(echo "$RESULTS" | jq '[.[] | select(.error)]')
COUNT=$(echo "$FAILED" | jq length)
echo "Failed: $COUNT"
echo "$FAILED" | jq '.[].input'Pattern 4: Build pipeline integration
# Compress all new images before committing
git diff --name-only --diff-filter=A HEAD | grep -E '\.(png|jpg)$' | while read f; do
img-convert "$f" -f webp --json 2>/dev/null | jq '"Compressed: \(.input) → \(.reduction)% smaller"'
donePattern 5: Node.js API in middleware
import { getInfo, convert } from '@dutchbase/img-convert'
async function handleUpload(buffer: Buffer): Promise<Buffer> {
const info = await getInfo(buffer)
if (info.width * info.height > 25_000_000) {
throw new Error('Image too large — max 25 megapixels')
}
return (await convert(buffer, {
format: 'webp',
quality: 85,
width: 2048, // cap at 2048px wide
removeMetadata: true,
...(info.hasAlpha ? {} : { background: '#ffffff' }),
})).buffer
}Format Support
| Format | Input | Output | Notes |
|---|---|---|---|
| JPEG | ✓ | ✓ | No alpha channel |
| PNG | ✓ | ✓ | Lossless, alpha supported |
| WebP | ✓ | ✓ | Animated WebP supported |
| AVIF | ✓ | ✓ | Slow encode, best compression |
| GIF | ✓ | ✓ | Animation preserved in GIF→GIF |
| TIFF | ✓ | ✓ | Print/archival |
| HEIC/HEIF | ✓ | ✗ | Input-only. Decoded via heic-convert. |
| SVG | ✓ | ✗ | Rasterized via librsvg. Output = SVG declared size unless overridden. |
| BMP | ✓ | ✗ | Input-only. Sharp has no BMP encoder. |
Processing Pipeline Order
Steps run in this fixed order. Each is independent and opt-in:
HEIC decode → decompression guard → metadata →
auto-rotate/rotate → flip/flop → crop → resize →
grayscale → normalize → blur → sharpen → trim →
background flatten → format encode → outputCrop runs before resize. Crop coordinates are in the original image's pixel space.
Background flatten runs last (before encode). It composites transparent areas onto the fill color. Required for JPEG output from any source with alpha.
Exit Codes
| Code | Meaning |
|---|---|
0 |
All files converted successfully |
1 |
One or more files failed, or fatal input error |
In --json mode, exit code 1 still writes a JSON array to stdout — failed items have "error": "..." fields. Parse stdout even on non-zero exit.
Common Mistakes
| Mistake | Fix |
|---|---|
Using heic, svg, or bmp as -f output |
These are input-only formats. Use jpeg, png, webp, etc. |
PNG/WebP → JPEG without --background |
Transparent pixels become black. Always pass --background "#ffffff" (or desired fill color). |
| Unquoted glob patterns in shell | Shell expands *.jpg before the CLI sees it. Always quote: "*.jpg". |
| Expecting upscaling by default | Upscaling is disabled. The image is returned at original size if smaller than target. |
Assuming --quality affects GIF |
GIF ignores quality entirely. |
Assuming --no-metadata removes ICC |
ICC color profile is always preserved regardless of --no-metadata. |
Checking only stdout for batch errors |
Errors appear in the JSON array as { "error": "..." } items. Check every item's shape. |
Running batch without --json in a script |
Without --json, output goes to stderr as human text. Use --json in any automated context. |
Installation
# Global CLI
npm install -g @dutchbase/img-convert
# Local dependency (Node.js API)
npm install @dutchbase/img-convert
# Verify
img-convert --help
img-convert info --help
img-convert batch --helpRequires Node.js >= 18.0.0.