Something went wrong. Try again.
Draws glyphs from zig-font onto z2d surfaces from positioned glyph runs, with fontconfig-based font loading and fallback.
Something went wrong. Try again.
6.1 kB · 154 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Scaling a glyph's image to the size it is drawn at.//!//! Embedded bitmaps are drawn for particular sizes and have to be scaled to//! any other. Shrinking uses a box filter: every destination pixel is the//! average of the source pixels it covers, weighted by how much of it each//! covers, so no source pixel is skipped however far the image shrinks.//! Enlarging either keeps the box filter, which leaves a pixel font's//! pixels square and sharp, or interpolates linearly between source pixel//! centers, which is kinder to a photographic emoji.//!//! The filter is separable, so it runs across the rows and then down the//! columns. Pixels are treated as `channels` independent bytes; for color,//! these must be pre-multiplied, as z2d's are, or a transparent pixel's//! color would bleed into its neighbors.
const std = @import("std");const Allocator = std.mem.Allocator;
pub const Filter = enum { /// Area averaging, at every scale. box, /// Area averaging when shrinking, linear interpolation when enlarging. smooth,};
/// Where the source image goes, in destination pixels: its top-left corner/// at `(x, y)`, each source pixel `scale_x` by `scale_y` destination pixels.pub const Placement = struct { x: f32, y: f32, scale_x: f32, scale_y: f32,};
/// Resample `src`, `src_w` by `src_h` pixels of `channels` bytes each, into/// `dst`, `dst_w` by `dst_h` pixels of the same, at `at`. Destination/// pixels the image does not reach are zero.pub fn image( gpa: Allocator, src: []const u8, src_w: usize, src_h: usize, channels: usize, dst: []u8, dst_w: usize, dst_h: usize, at: Placement, filter: Filter,) Allocator.Error!void { std.debug.assert(src.len == src_w * src_h * channels); std.debug.assert(dst.len == dst_w * dst_h * channels);
// Across: src_h rows of dst_w pixels. const rows = try gpa.alloc(f32, src_h * dst_w * channels); defer gpa.free(rows); const line_in = try gpa.alloc(f32, @max(src_w, src_h)); defer gpa.free(line_in); const line_out = try gpa.alloc(f32, @max(dst_w, dst_h)); defer gpa.free(line_out);
for (0..src_h) |r| for (0..channels) |c| { for (line_in[0..src_w], 0..) |*v, x| v.* = @floatFromInt(src[(r * src_w + x) * channels + c]); line(line_out[0..dst_w], line_in[0..src_w], at.x, at.scale_x, filter); for (line_out[0..dst_w], 0..) |v, x| rows[(r * dst_w + x) * channels + c] = v; };
// Down: dst_w columns of dst_h pixels. for (0..dst_w) |x| for (0..channels) |c| { for (line_in[0..src_h], 0..) |*v, r| v.* = rows[(r * dst_w + x) * channels + c]; line(line_out[0..dst_h], line_in[0..src_h], at.y, at.scale_y, filter); for (line_out[0..dst_h], 0..) |v, y| { dst[(y * dst_w + x) * channels + c] = @intFromFloat(std.math.clamp(@round(v), 0, 255)); } };}
/// One row or column: source pixel `i` covers `[start + i * scale,/// start + (i + 1) * scale)` of `dst`.fn line(dst: []f32, src: []const f32, start: f32, scale: f32, filter: Filter) void { @memset(dst, 0); if (filter == .smooth and scale > 1) return interpolate(dst, src, start, scale); for (src, 0..) |v, i| { if (v == 0) continue; spread(dst, v, start + @as(f32, @floatFromInt(i)) * scale, scale); }}
/// Add `v` over `[a, a + len)` of `dst`, in proportion to how much of each/// pixel that interval covers.fn spread(dst: []f32, v: f32, a: f32, len: f32) void { const b = a + len; var d: usize = @intFromFloat(@max(@floor(a), 0)); while (d < dst.len) : (d += 1) { const lo: f32 = @floatFromInt(d); if (lo >= b) break; const overlap = @min(lo + 1, b) - @max(lo, a); if (overlap > 0) dst[d] += v * overlap; }}
/// Each destination pixel's center, mapped back into the source, takes the/// two source pixels whose centers it falls between, weighted by distance./// Past the outermost centers the image fades to nothing over half a source/// pixel, as if it were surrounded by transparency, which it is.fn interpolate(dst: []f32, src: []const f32, start: f32, scale: f32) void { const n: f32 = @floatFromInt(src.len); for (dst, 0..) |*o, d| { const u = (@as(f32, @floatFromInt(d)) + 0.5 - start) / scale - 0.5; if (u <= -1 or u >= n) continue; const f = @floor(u); const w = u - f; const lo = sampleAt(src, f); const hi = sampleAt(src, f + 1); o.* = lo * (1 - w) + hi * w; }}
fn sampleAt(src: []const f32, i: f32) f32 { if (i < 0 or i >= @as(f32, @floatFromInt(src.len))) return 0; return src[@intFromFloat(i)];}
test "the box filter copies at scale one and spreads otherwise" { var dst: [4]f32 = undefined; line(&dst, &.{ 255, 0, 255 }, 1, 1, .box); try std.testing.expectEqualSlices(f32, &.{ 0, 255, 0, 255 }, &dst); line(&dst, &.{ 255, 0 }, 0, 2, .box); try std.testing.expectEqualSlices(f32, &.{ 255, 255, 0, 0 }, &dst); line(&dst, &.{ 255, 255 }, 0.5, 1, .box); try std.testing.expectEqualSlices(f32, &.{ 127.5, 255, 127.5, 0 }, &dst); // Halving averages each pair. line(dst[0..2], &.{ 255, 0, 100, 100 }, 0, 0.5, .smooth); try std.testing.expectEqualSlices(f32, &.{ 127.5, 100 }, dst[0..2]);}
test "enlarging smoothly interpolates between pixel centers" { var dst: [4]f32 = undefined; line(&dst, &.{ 0, 200 }, 0, 2, .smooth); // Centers at 0.5, 1.5, 2.5, 3.5 map back to -0.25, 0.25, 0.75, 1.25. try std.testing.expectEqualSlices(f32, &.{ 0, 50, 150, 150 }, &dst);}
test "a whole image, two channels, shrunk by half" { const src = [_]u8{ 255, 0, 255, 0, 0, 0, 0, 0, 255, 0, 255, 0, 0, 0, 0, 0, }; var dst: [4]u8 = undefined; try image(std.testing.allocator, &src, 4, 2, 2, &dst, 2, 1, .{ .x = 0, .y = 0, .scale_x = 0.5, .scale_y = 0.5 }, .smooth); try std.testing.expectEqualSlices(u8, &.{ 255, 0, 0, 0 }, &dst);}