Combine images in JavaScript
combine-images is a small, zero-dependency JavaScript package that combines images side by side, vertically or in a grid on a canvas. It runs in the browser, in Web Workers and in Node, and it is the layout code behind the PhotoMerge editor.
npm · GitHub · MIT license · v1.0.0
npm install combine-images
Try it here
This demo runs the package in your browser. Nothing is uploaded.
Choose images or use the samples.
Combine images in the browser
Pass files, Blobs, URLs or loaded images. The result is a canvas you can show, encode or upload.
import { 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, import it straight from a CDN:
<script type="module">
import { combineImages } from 'https://cdn.jsdelivr.net/npm/combine-images@1/src/index.js';
</script>
Combine images in Node
Node has no built-in canvas, so pass createCanvas and loadImage from @napi-rs/canvas (or the canvas package). Sources can then be file paths, URLs or Buffers.
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'));
Layouts
direction | What it does | Size options |
|---|---|---|
horizontal (default) | One row. Each image keeps its shape and is scaled to the same height. | height, default the shortest image |
vertical | One column. Each 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, cellWidth, cellHeight, fit (cover or contain) |
Scaling to a shared height or width avoids the usual problem with merging photos of different sizes: no stretched faces and no uneven strips of background. The grid crops to fill each cell by default; use fit: 'contain' to show whole images instead.
Options
| Option | Default | Notes |
|---|---|---|
gap | 0 | Space between images, in output pixels |
padding | 0 | Space around the result |
background | '#ffffff' | Any canvas color. null keeps it transparent; export PNG or WebP |
radius | 0 | Corner radius of each image |
labels | [] | Text per image, by index, e.g. Before and After |
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 a new canvas element | Where to draw |
loadImage | createImageBitmap in browsers | How to load sources that are not images yet |
Use only the layout math
computeLayout takes sizes and returns the output size and one rectangle per image. It never touches a canvas, so you can render with WebGL, SVG, sharp or a server-side pipeline, or show a preview before decoding full-size files.
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 },
// { index: 1, x: 1210, y: 0, width: 533.33, height: 800 }] }
drawLayout(canvas, images, layout, { background: '#000', scale: 0.25 });
Combine images in a Web Worker
Large photos can block the page while they decode and draw. In a worker, combineImages uses OffscreenCanvas automatically.
// worker.js: combine off the main thread with OffscreenCanvas
import { combineImages } from 'combine-images';
self.onmessage = async ({ data: files }) => {
const canvas = await combineImages(files, { direction: 'vertical', maxSize: 4000 });
self.postMessage(await canvas.convertToBlob({ type: 'image/webp', quality: 0.9 }));
};
Canvas size limits
Browsers cap canvas size: Safari at about 16.7 million pixels, Chrome and Firefox at about 268 million, and 32,767 pixels on a side. Ten 12-megapixel photos in one row can pass those limits, so set maxSize when users choose many large images. JPEG encoding also has a 65,535 pixel limit per side.
No code needed?
To merge a few photos by hand, use the free PhotoMerge editor: side by side, vertically or as a grid, with no upload, signup or watermark.
Questions or bugs: GitHub issues · Contact