Skip to content

Photo Merge — Swift source

Combine photos side by side, stacked, or in a grid — uniform cells, adjustable gap, one PNG out. Runs entirely in your browser.

This is the Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.

// photo-merge — Swift port: side-by-side merge cell layout.

import Foundation

/// A decoded image's dimensions. In the browser tool the bitmap is
/// drawImage'd into its letterboxed cell; the geometry here is the
/// deterministic part of that pipeline.
struct ImageSize {
    var width: Int
    var height: Int
}

/// Where one image lands on the merged canvas.
struct Placement {
    var scale: Double
    var x: Int
    var y: Int
    var w: Int
    var h: Int
}

struct MergedLayout {
    var canvasW: Int
    var canvasH: Int
    var placements: [Placement]
}

/// Mirrors mergeSideBySide in typescript.ts: cellW/cellH are the largest
/// input dimensions, the canvas is n cells + (n + 1) gaps wide and one
/// cell + 2 gaps tall, and each image is scaled by
/// min(cellW / w, cellH / h) and centered inside its cell.
/// `rounded(.awayFromZero)` matches JS Math.round for positive values.
func mergeLayout(_ images: [ImageSize], gap: Int = 12) -> MergedLayout {
    let n = images.count
    let cellW = images.map(\.width).max() ?? 0
    let cellH = images.map(\.height).max() ?? 0

    func round(_ v: Double) -> Int { Int(v.rounded(.awayFromZero)) }

    var placements: [Placement] = []
    placements.reserveCapacity(n)
    for (i, img) in images.enumerated() {
        let scale = min(Double(cellW) / Double(img.width),
                        Double(cellH) / Double(img.height))
        let w = round(Double(img.width) * scale)
        let h = round(Double(img.height) * scale)
        placements.append(Placement(
            scale: scale,
            x: gap + i * (cellW + gap) + round(Double(cellW - w) / 2),
            y: gap + round(Double(cellH - h) / 2),
            w: w,
            h: h))
    }
    return MergedLayout(canvasW: n * cellW + (n + 1) * gap,
                        canvasH: cellH + 2 * gap,
                        placements: placements)
}

// 800x600 + 600x800 + 1024x768 with a 12px gap -> 3120x824 canvas.
let layout = mergeLayout([
    ImageSize(width: 800, height: 600),
    ImageSize(width: 600, height: 800),
    ImageSize(width: 1024, height: 768),
], gap: 12)
print("canvas \(layout.canvasW)x\(layout.canvasH)")
for (i, p) in layout.placements.enumerated() {
    let scale = String(format: "%.4f", p.scale)
    print("image \(i): (\(p.x), \(p.y)) \(p.w)x\(p.h) scale=\(scale)")
}

Also available in 8 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →