merge_images
Combine multiple images into one by arranging them horizontally, vertically, or in a grid layout with customizable spacing and background color.
Instructions
Merge multiple images into a single image by arranging them horizontally, vertically, or in a grid.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| input_paths | Yes | Ordered list of absolute paths to the images to merge (minimum 2) | |
| layout | No | Arrangement: horizontal (side by side), vertical (stacked), or grid (auto columns) | horizontal |
| gap | No | Gap in pixels between images (default 0) | |
| background | No | Background/gap fill color as hex (default #ffffff) | #ffffff |
| output_path | Yes | Absolute path for the output file (format inferred from extension) |
Implementation Reference
- index.js:237-317 (handler)The handler function that performs the logic of merging multiple images based on input parameters (layout, gap, etc.) using the sharp library.
async ({ input_paths, layout = "horizontal", gap = 0, background = "#ffffff", output_path }) => { try { // Load metadata for all images const metas = await Promise.all(input_paths.map(async (p) => { await fs.access(p); const meta = await sharp(p).metadata(); return { path: p, width: meta.width, height: meta.height }; })); // Parse background color let bg = { r: 255, g: 255, b: 255, alpha: 1 }; const hex = background.replace('#', ''); if (hex.length === 6) { bg = { r: parseInt(hex.slice(0,2),16), g: parseInt(hex.slice(2,4),16), b: parseInt(hex.slice(4,6),16), alpha: 1 }; } let canvasW, canvasH, positions; if (layout === "horizontal") { canvasH = Math.max(...metas.map(m => m.height)); canvasW = metas.reduce((sum, m) => sum + m.width, 0) + gap * (metas.length - 1); positions = []; let x = 0; for (const m of metas) { positions.push({ x, y: Math.floor((canvasH - m.height) / 2) }); x += m.width + gap; } } else if (layout === "vertical") { canvasW = Math.max(...metas.map(m => m.width)); canvasH = metas.reduce((sum, m) => sum + m.height, 0) + gap * (metas.length - 1); positions = []; let y = 0; for (const m of metas) { positions.push({ x: Math.floor((canvasW - m.width) / 2), y }); y += m.height + gap; } } else { // grid: auto columns = ceil(sqrt(n)) const cols = Math.ceil(Math.sqrt(metas.length)); const rows = Math.ceil(metas.length / cols); const cellW = Math.max(...metas.map(m => m.width)); const cellH = Math.max(...metas.map(m => m.height)); canvasW = cols * cellW + gap * (cols - 1); canvasH = rows * cellH + gap * (rows - 1); positions = metas.map((m, i) => { const col = i % cols; const row = Math.floor(i / cols); return { x: col * (cellW + gap) + Math.floor((cellW - m.width) / 2), y: row * (cellH + gap) + Math.floor((cellH - m.height) / 2), }; }); } // Build composite layers const composites = await Promise.all(metas.map(async (m, i) => ({ input: await sharp(m.path).toBuffer(), left: positions[i].x, top: positions[i].y, }))); const outExt = path.extname(output_path).slice(1).toLowerCase() || "png"; const formatMap = { jpg: "jpeg" }; const fmt = formatMap[outExt] || outExt; await sharp({ create: { width: canvasW, height: canvasH, channels: 4, background: bg }, }) .composite(composites) .toFormat(fmt) .toFile(output_path); const stat = await fs.stat(output_path); return { content: [{ type: "text", text: JSON.stringify({ success: true, output_path, width: canvasW, height: canvasH, size_bytes: stat.size }) }], }; } catch (err) { return { isError: true, content: [{ type: "text", text: `Error: ${err.message}` }] }; } } ); - index.js:227-236 (registration)Registration of the "merge_images" tool with schema definitions for input arguments using Zod.
server.tool( "merge_images", "Merge multiple images into a single image by arranging them horizontally, vertically, or in a grid.", { input_paths: z.array(z.string()).min(2).describe("Ordered list of absolute paths to the images to merge (minimum 2)"), layout: z.enum(["horizontal", "vertical", "grid"]).optional().default("horizontal").describe("Arrangement: horizontal (side by side), vertical (stacked), or grid (auto columns)"), gap: z.number().int().min(0).optional().default(0).describe("Gap in pixels between images (default 0)"), background: z.string().optional().default("#ffffff").describe("Background/gap fill color as hex (default #ffffff)"), output_path: z.string().describe("Absolute path for the output file (format inferred from extension)"), },