Contact sheet layout generator
Planning a contact sheet should not require trial-and-error resizing. Provide the pixel width and height of every image plus the widest sheet you can use, and this calculator returns a practical grid with a uniform thumbnail box.
Run — free
It reports the number of columns and rows, thumbnail width and height, and final sheet dimensions. The layout preserves room for every source aspect ratio, stays within the width limit, and is produced by deterministic arithmetic that gives the same result every time.
Prepare the image dimensions and width limit
Start with one record for every image that will appear on the contact sheet. Each record needs a width and height in pixels, and both values must be positive whole numbers. The files themselves are not uploaded or decoded: only their dimensions are needed. Include every image because the number of records determines how many grid cells must be available, while the widest and tallest source dimensions define a common bounding-box shape that can contain the full collection without cropping. Then set max_sheet_width to the pixel width available in your document, export, gallery panel, or print workflow. The calculator treats that value as a hard ceiling. It does not reserve space for external page margins, captions, gutters, borders, or printer bleed, so subtract those allowances before submitting if another system will add them later. A list with zero images is rejected because columns, rows, and thumbnail size have no useful meaning when there is nothing to place. Invalid, fractional, zero, or negative dimensions are also rejected rather than silently repaired, keeping automated layout pipelines predictable.
Understand how the grid is selected
The solver evaluates every feasible column count from one through the number of images. For each candidate it calculates the required row count with a ceiling operation, divides the maximum sheet width into equal integer-width cells, and derives a matching thumbnail height from the overall source-dimension envelope. This creates a uniform thumbnail box suitable for contain-style placement: a renderer scales each image proportionally until it fits inside that box, potentially leaving unused space on one axis but never requiring a crop. The solver compares the resulting sheet width and height and selects the candidate closest to a square. A compact, balanced sheet is generally easier to scan and avoids the extreme one-column strip that merely maximizing thumbnail size would produce. If two candidates are equally square, the one offering greater thumbnail area wins. If they remain tied, fewer columns wins, which makes the final choice explicit and stable. Thumbnail width is also capped at the widest source width, avoiding unnecessary upscaling when a generous sheet limit is supplied. All calculations use integer pixels and deterministic tie-breaking.
Use the result in a renderer or production pipeline
The response supplies count, columns, rows, thumbnail_size, sheet_size, max_sheet_width, and the fit mode. Create a canvas using sheet_size.width and sheet_size.height, then place item index i at column i modulo columns and row floor(i divided by columns). Each cell has the returned thumbnail width and height. Scale the source with a contain operation, center it in the cell if desired, and preserve its original aspect ratio. Because the last row may not be full, your renderer can leave trailing cells blank or center that row as a separate presentation choice; the calculated canvas deliberately keeps simple rectangular grid geometry. The result excludes gutters and captions. To add a ten-pixel gap, for example, either reserve the total gap width before calling the calculator or treat the returned thumbnail dimensions as a starting point and reduce each cell consistently. The endpoint is useful both interactively and in repeatable batch jobs: identical input produces identical output, it makes no network requests, and it never inspects image content. API use costs $0.002 per request, while browser execution can use the same pure calculation locally.
What you can do with it
Plan a photo proof sheet
Choose a balanced thumbnail grid that fits a fixed export width before loading or resizing the original photographs.
Build a media-library preview
Turn stored image metadata into deterministic columns, rows, and cell dimensions for a gallery renderer.
Prepare dataset overview pages
Lay out mixed portrait, landscape, and square samples in uniform contain-style boxes without cropping.
FAQ
What does it cost?
API execution costs $0.002 per request. The calculation is also suitable for free local browser execution.
Does this resize or combine my images?
No. It returns layout numbers only; your image renderer performs the actual scaling and composition.
Will portrait and landscape images be cropped?
No cropping is required. The thumbnail size is a bounding box intended for aspect-preserving contain placement.
Are gaps, captions, or margins included?
No. Reserve space for gutters, labels, borders, margins, or bleed before choosing the maximum sheet width.
Why does an empty image list cause an error?
A grid for zero items has no meaningful row count or thumbnail size, so the input is rejected explicitly.
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, by email and from Telegram — and soon from our app too.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/image/contact-sheet-layout \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"images":[{"width":1600,"height":900},{"width":1200,"height":1600},{"width":2048,"height":1365},{"width":800,"height":800},{"width":1920,"height":1080}],"max_sheet_width":1200}'const res = await fetch("https://api.kit.forhosting.com/image/contact-sheet-layout", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"images": [
{
"width": 1600,
"height": 900
},
{
"width": 1200,
"height": 1600
},
{
"width": 2048,
"height": 1365
},
{
"width": 800,
"height": 800
},
{
"width": 1920,
"height": 1080
}
],
"max_sheet_width": 1200
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/image/contact-sheet-layout",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"images": [
{
"width": 1600,
"height": 900
},
{
"width": 1200,
"height": 1600
},
{
"width": 2048,
"height": 1365
},
{
"width": 800,
"height": 800
},
{
"width": 1920,
"height": 1080
}
],
"max_sheet_width": 1200
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/image/contact-sheet-layout", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"images":[{"width":1600,"height":900},{"width":1200,"height":1600},{"width":2048,"height":1365},{"width":800,"height":800},{"width":1920,"height":1080}],"max_sheet_width":1200}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"images":[{"width":1600,"height":900},{"width":1200,"height":1600},{"width":2048,"height":1365},{"width":800,"height":800},{"width":1920,"height":1080}],"max_sheet_width":1200}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/image/contact-sheet-layout", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"images": [
{
"width": 1600,
"height": 900
},
{
"width": 1200,
"height": 1600
},
{
"width": 2048,
"height": 1365
},
{
"width": 800,
"height": 800
},
{
"width": 1920,
"height": 1080
}
],
"max_sheet_width": 1200
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "image.contact_sheet_layout",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Limits
max_mb | 15 |
max_megapixels | 12 |
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |