Skip to content

Photo Merge — Kotlin 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 Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.

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

/** 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. */
data class ImageSize(val width: Int, val height: Int)

/** Where one image lands on the merged canvas. */
data class Placement(val scale: Double, val x: Int, val y: Int, val w: Int, val h: Int)

data class MergedLayout(val canvasW: Int, val canvasH: Int, val placements: List<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.
 *  Math.round rounds half up, matching JS exactly. */
fun mergeLayout(images: List<ImageSize>, gap: Int): MergedLayout {
    val n = images.size
    val cellW = images.maxOf { it.width }
    val cellH = images.maxOf { it.height }

    val placements = images.mapIndexed { i, img ->
        val scale = minOf(
            cellW.toDouble() / img.width,
            cellH.toDouble() / img.height,
        )
        val w = Math.round(img.width * scale).toInt()
        val h = Math.round(img.height * scale).toInt()
        Placement(
            scale,
            gap + i * (cellW + gap) + Math.round((cellW - w) / 2.0).toInt(),
            gap + Math.round((cellH - h) / 2.0).toInt(),
            w,
            h,
        )
    }
    return MergedLayout(n * cellW + (n + 1) * gap, cellH + 2 * gap, placements)
}

fun main() {
    // 800x600 + 600x800 + 1024x768 with a 12px gap -> 3120x824 canvas.
    val layout = mergeLayout(
        listOf(
            ImageSize(800, 600),
            ImageSize(600, 800),
            ImageSize(1024, 768),
        ),
        gap = 12,
    )
    println("canvas ${layout.canvasW}x${layout.canvasH}")
    layout.placements.forEachIndexed { i, p ->
        println("image $i: (${p.x}, ${p.y}) ${p.w}x${p.h} scale=${"%.4f".format(p.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 →