Combine images side by side, vertically or in a grid, with optional gaps, padding, background, rounded corners and labels. Zero dependencies. Runs in the browser, in Web Workers (OffscreenCanvas) and in Node with @napi-rs/canvas.
Docs and live demo: photomerge.app/developers/combine-images
Don't need code? The same layouts run as a free, no-upload editor at PhotoMerge.
npm install combine-imagesimport { combineImages, canvasToBlob } from 'combine-images';
// Files from <input type="file" multiple>, Blobs, URLs or loaded <img> elements
const canvas = await combineImages(input.files, {
direction: 'horizontal', // 'horizontal' | 'vertical' | 'grid'
gap: 16,
padding: 16,
background: '#ffffff',
labels: ['Before', 'After'],
});
const blob = await canvasToBlob(canvas, 'image/jpeg', 0.92);Without a bundler, load it from a CDN:
<script type="module">
import { combineImages } from 'https://cdn.jsdelivr.net/npm/combine-images@1/src/index.js';
</script>import { writeFile } from 'node:fs/promises';
import { createCanvas, loadImage } from '@napi-rs/canvas';
import { combineImages } from 'combine-images';
const canvas = await combineImages(['a.jpg', 'b.jpg', 'c.jpg'], {
direction: 'grid',
gap: 12,
createCanvas,
loadImage,
});
await writeFile('grid.png', await canvas.encode('png'));direction |
What it does | Size options |
|---|---|---|
horizontal (default) |
One row. Every image keeps its shape and is scaled to the same height. | height (default: the shortest image) |
vertical |
One column. Every image keeps its shape and is scaled to the same width. | width (default: the narrowest image) |
grid |
Equal cells; a short last row is centred. | columns ('auto' or a number), cellWidth, cellHeight, fit: 'cover' | 'contain' |
| Option | Default | |
|---|---|---|
gap |
0 |
Space between images, in output pixels |
padding |
0 |
Space around the result |
background |
'#ffffff' |
Any canvas color; null for transparent (use PNG or WebP) |
radius |
0 |
Corner radius of each image |
labels |
[] |
Text per image, by index |
labelPosition |
'bottom-left' |
top-left, top-center, bottom-left, bottom-center |
labelColor, labelBackground, labelFont, labelSize |
white on 60% black, system font, 0.065 |
labelSize is a share of the shorter cell side |
maxSize |
none | Scale the result down so its longest side fits |
scale |
1 |
Multiply the output size |
canvas / createCanvas |
OffscreenCanvas or <canvas> |
Where to draw |
loadImage |
createImageBitmap in browsers |
How to load sources that are not images yet |
computeLayout(sizes, options) is pure math: give it { width, height } objects and it returns the output size and a rectangle per image, so you can render with WebGL, SVG, sharp or anything else.
import { computeLayout, drawLayout } from 'combine-images';
const layout = computeLayout([{ width: 1200, height: 800 }, { width: 800, height: 1200 }], { gap: 10 });
// { width: 1743, height: 800, cells: [{ index: 0, x: 0, y: 0, width: 1200, height: 800 }, ...] }
drawLayout(canvas, images, layout, { background: '#000', scale: 0.25 }); // e.g. a quick previewBrowsers cap canvas size (Safari at about 16.7 million pixels, Chrome and Firefox at about 268 million, and 32,767 px per side). Use maxSize when combining many large photos.
MIT © PhotoMerge