Image Resize & Watermark — Zig source
Batch-resize images by width, height or percentage and stamp a text watermark with position, opacity and size control — entirely in your browser, nothing uploaded.
This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.
//! Image Resize & Watermark — Zig (0.13, std only) port of the image-resize-and-watermark tool.
//! Zig's std has no image codec or font rasterizer, so this port covers the
//! pure layout half of the pipeline (target size + watermark geometry); pixel
//! decode, resampling, and WebP encode live in an external lib at the edges.
const std = @import("std");
/// Watermark style constants, mirroring the TS canvas code.
pub const margin: i32 = 8; // bottom-right margin, px
pub const font_scale: f64 = 6.0 / 100.0; // fontSize = min(w, h) * 6 / 100
pub const fill_alpha: f64 = 0.7; // white fillText alpha
pub const stroke_alpha: f64 = 0.55; // dark strokeText alpha
/// JS Math.round parity: half away from zero, spelled @floor(x + 0.5).
fn jsRound(x: f64) i32 {
const v = @floor(x + 0.5);
if (v < 1.0) return 1;
return @intFromFloat(v);
}
/// Target size for an aspect-preserving resize to `target_width`
/// (h = Math.round((height / width) * targetWidth)).
pub fn targetSize(width: i32, height: i32, target_width: i32) struct { w: i32, h: i32 } {
const h = jsRound(@as(f64, @floatFromInt(height)) / @as(f64, @floatFromInt(width)) *
@as(f64, @floatFromInt(target_width)));
return .{ .w = target_width, .h = h };
}
/// Watermark font size in px: 6% of the shorter output side, rounded.
pub fn fontSize(w: i32, h: i32) i32 {
return jsRound(@as(f64, @floatFromInt(@min(w, h))) * font_scale);
}
/// Outline width for the dark stroke: max(1, fontSize / 14) — fractional, as
/// canvas lineWidth is.
pub fn strokeWidth(font_size: i32) f64 {
return @max(1.0, @as(f64, @floatFromInt(font_size)) / 14.0);
}
/// Approximate measureText width for a system sans-serif font: ~0.56 em
/// average advance per scalar value (a real canvas needs a font rasterizer,
/// which lives outside std).
pub fn textWidth(text: []const u8, font_size: i32) f64 {
const n: f64 = @floatFromInt(std.unicode.utf8CountCodepoints(text) catch text.len);
return n * @as(f64, @floatFromInt(font_size)) * 0.56;
}
/// Bottom-right watermark origin — the TS x = w - textWidth - 8 (left edge of
/// the text) and y = h - 8 (baseline). CG-style renderers anchor differently;
/// the values are the same contract.
pub fn origin(w: i32, h: i32, text_width: f64) struct { x: f64, y: i32 } {
return .{
.x = @as(f64, @floatFromInt(w)) - text_width - @as(f64, @floatFromInt(margin)),
.y = h - margin,
};
}
test "1280-wide resize of a 4032x3024 photo" {
const t = targetSize(4032, 3024, 1280);
try std.testing.expectEqual(@as(i32, 1280), t.w);
try std.testing.expectEqual(@as(i32, 960), t.h); // 3024 / 4032 * 1280
try std.testing.expectEqual(@as(i32, 58), fontSize(1280, 960)); // 6% of the 960 px short side
try std.testing.expectApproxEqAbs(@as(f64, 58.0 / 14.0), strokeWidth(58), 0.001);
const o = origin(1280, 960, textWidth("CosmoDev", 58));
try std.testing.expectEqual(@as(i32, 952), o.y);
try std.testing.expect(o.x > 900 and o.x < 950); // 1280 - ~8*58*0.56 - 8
}
pub fn main() void {
const t = targetSize(4032, 3024, 1280);
std.debug.print("target {d}x{d}, watermark font {d}px\n", .{ t.w, t.h, fontSize(t.w, t.h) });
}
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 →