Skip to content

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

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

using System;
using System.Collections.Generic;
using System.Linq;

// 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.
readonly struct ImageSize
{
    public ImageSize(int width, int height) => (Width, Height) = (width, height);
    public int Width { get; }
    public int Height { get; }
}

// Where one image lands on the merged canvas.
readonly struct Placement
{
    public Placement(double scale, int x, int y, int w, int h) =>
        (Scale, X, Y, W, H) = (scale, x, y, w, h);
    public double Scale { get; }
    public int X { get; }
    public int Y { get; }
    public int W { get; }
    public int H { get; }
}

static class PhotoMerge
{
    // JS Math.round rounds half up; Math.Round(away-from-zero) matches
    // it for the positive coordinates and dimensions used here.
    static int Round(double v) => (int)Math.Round(v, MidpointRounding.AwayFromZero);

    // 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.
    public static (int CanvasW, int CanvasH, List<Placement> Placements)
        MergeLayout(IReadOnlyList<ImageSize> images, int gap = 12)
    {
        int n = images.Count;
        int cellW = images.Max(i => i.Width);
        int cellH = images.Max(i => i.Height);
        var placements = new List<Placement>(n);

        for (int i = 0; i < n; i++)
        {
            double scale = Math.Min(
                cellW / (double)images[i].Width,
                cellH / (double)images[i].Height);
            int w = Round(images[i].Width * scale);
            int h = Round(images[i].Height * scale);
            placements.Add(new Placement(
                scale,
                gap + i * (cellW + gap) + Round((cellW - w) / 2.0),
                gap + Round((cellH - h) / 2.0),
                w, h));
        }
        return (n * cellW + (n + 1) * gap, cellH + 2 * gap, placements);
    }
}

class Program
{
    static void Main()
    {
        // 800x600 + 600x800 + 1024x768 with a 12px gap -> 3120x824 canvas.
        var (canvasW, canvasH, placements) = PhotoMerge.MergeLayout(
            new[] { new ImageSize(800, 600), new ImageSize(600, 800), new ImageSize(1024, 768) });
        Console.WriteLine($"canvas {canvasW}x{canvasH}");
        for (int i = 0; i < placements.Count; i++)
        {
            var p = placements[i];
            Console.WriteLine($"image {i}: ({p.X}, {p.Y}) {p.W}x{p.H} scale={p.Scale:0.####}");
        }
    }
}

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 →