Photo Merge — Zig 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 Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.
// photo-merge — Zig port: side-by-side merge cell layout.
const std = @import("std");
/// 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.
const ImageSize = struct { width: i32, height: i32 };
/// Where one image lands on the merged canvas.
const Placement = struct { scale: f64, x: i32, y: i32, w: i32, h: i32 };
const MergedLayout = struct {
canvas_w: i32,
canvas_h: i32,
placements: []Placement,
};
/// JS Math.round (half up for positives): @round gives half-away-from-
/// zero, identical on the non-negative values used here.
fn jsRound(v: f64) i32 {
return @intFromFloat(@round(v));
}
/// 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.
/// Caller owns the returned placements slice (allocated from `alloc`).
fn mergeLayout(alloc: std.mem.Allocator, images: []const ImageSize, gap: i32) !MergedLayout {
const n: i32 = @intCast(images.len);
var cell_w: i32 = 0;
var cell_h: i32 = 0;
for (images) |img| {
cell_w = @max(cell_w, img.width);
cell_h = @max(cell_h, img.height);
}
const placements = try alloc.alloc(Placement, images.len);
for (images, 0..) |img, i| {
const idx: i32 = @intCast(i);
const fw: f64 = @floatFromInt(img.width);
const fh: f64 = @floatFromInt(img.height);
const scale = @min(@as(f64, @floatFromInt(cell_w)) / fw,
@as(f64, @floatFromInt(cell_h)) / fh);
const w = jsRound(fw * scale);
const h = jsRound(fh * scale);
placements[i] = .{
.scale = scale,
.x = gap + idx * (cell_w + gap) + jsRound(@as(f64, @floatFromInt(cell_w - w)) / 2),
.y = gap + jsRound(@as(f64, @floatFromInt(cell_h - h)) / 2),
.w = w,
.h = h,
};
}
return .{ .canvas_w = n * cell_w + (n + 1) * gap,
.canvas_h = cell_h + 2 * gap, .placements = placements };
}
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
// 800x600 + 600x800 + 1024x768 with a 12px gap -> 3120x824 canvas.
const images = [_]ImageSize{
.{ .width = 800, .height = 600 },
.{ .width = 600, .height = 800 },
.{ .width = 1024, .height = 768 },
};
const layout = try mergeLayout(arena.allocator(), &images, 12);
const out = std.io.getStdOut().writer();
try out.print("canvas {d}x{d}\n", .{ layout.canvas_w, layout.canvas_h });
for (layout.placements, 0..) |p, i| {
try out.print("image {d}: ({d}, {d}) {d}x{d} scale={d:.4}\n",
.{ i, p.x, p.y, p.w, p.h, 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 →