Something went wrong. Try again.
A rainbow login banner drawn in a real font and shown with the kitty graphics protocol, written in Zig.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Drawing the banner text: shaped, rendered, and cropped to its ink, at//! the size that makes the ink exactly as tall as asked.//!//! How tall a font's letters come out at a given size depends on the font//! and on the text — a name with no capitals or descenders is shorter than//! the font's line height says — so the size is found by measuring. The//! text is drawn once at a reference size, the size scaled by how far its//! ink missed the height wanted, and drawn again; a text too wide for the//! terminal is shrunk the same way.
const std = @import("std");const Allocator = std.mem.Allocator;const z2d = @import("z2d");const renderer = @import("font_renderer");const loader = @import("font_loader");const text = @import("font_shaper_text");
/// What to draw the text with: a fontconfig fallback list, or one face/// loaded from a file.pub const Fonts = union(enum) { list: *loader.FontList, face: *const renderer.Face,};
pub const Error = text.Error || error{ /// The text drew no ink at all: it was blank, or no font had its /// characters. NothingToDraw, /// The text was too large to draw a surface for. InvalidWidth, InvalidHeight,};
/// The size the first measuring pass draws at, in pixels per em.const reference_size: f32 = 100;
/// The text drawn in white on a transparent surface exactly its ink's size,/// as close to `height` pixels tall as it can be without being taller, and/// no wider than `max_width`. The caller owns the surface.pub fn render(gpa: Allocator, fonts: Fonts, banner_text: []const u8, height: u32, max_width: ?u32) Error!z2d.Surface { var size = reference_size; var drawn = try draw(gpa, fonts, banner_text, size); // Hinting moves edges to whole pixels, so the ink is not quite // proportional to the size; a few rounds settle it. var round: usize = 0; while (true) : (round += 1) { const ink_h: f32 = @floatFromInt(drawn.ink.h); const ink_w: f32 = @floatFromInt(drawn.ink.w); var factor = @as(f32, @floatFromInt(height)) / ink_h; if (max_width) |mw| factor = @min(factor, @as(f32, @floatFromInt(mw)) / ink_w); const fits = drawn.ink.h <= height and (max_width == null or drawn.ink.w <= max_width.?); if (round > 0 and fits and factor < 1.05) break; if (round >= 4 and fits) break; if (round >= 8) break; // Aim a hair under, so that rounding up lands on the target rather // than a pixel over it. size *= if (round == 0) factor else factor * 0.998; drawn.surface.deinit(gpa); drawn = try draw(gpa, fonts, banner_text, size); } defer drawn.surface.deinit(gpa); return crop(gpa, &drawn.surface, drawn.ink);}
const Box = struct { x: i32, y: i32, w: u32, h: u32 };
const Drawn = struct { surface: z2d.Surface, ink: Box,};
/// The text drawn at `size` on a surface with room around it on every side,/// and where on that surface its ink is.fn draw(gpa: Allocator, fonts: Fonts, banner_text: []const u8, size: f32) Error!Drawn { var laid = try Laid.init(gpa, fonts, size, banner_text); defer laid.deinit(gpa);
// A glyph may reach outside its advance and above the font's ascender, // so the margins are generous: an em on every side. const margin = size; const width: i32 = @intFromFloat(@ceil(@max(laid.endX(), 0) + 2 * margin)); const height: i32 = @intFromFloat(@ceil(3 * size)); var sfc = try z2d.Surface.init(.image_surface_rgba, gpa, width, height); errdefer sfc.deinit(gpa);
for (laid.runs()) |run| { try renderer.drawRun(gpa, &sfc, run, margin, 2 * size, .{ .color = .{ .rgba = .{ .r = 255, .g = 255, .b = 255, .a = 255 } }, }); } const ink = inkBox(&sfc) orelse return error.NothingToDraw; return .{ .surface = sfc, .ink = ink };}
/// The glyphs of the whole text, however they were shaped.const Laid = union(enum) { layout: text.Layout, shaped: struct { shaped: text.Shaped, run: renderer.Run },
fn init(gpa: Allocator, fonts: Fonts, size: f32, banner_text: []const u8) Error!Laid { switch (fonts) { .list => |list| return .{ .layout = try text.layout(gpa, list, size, banner_text, .{}) }, .face => |face| { const shaped = try text.shapeRun(gpa, face, size, banner_text, 0, banner_text.len, null, .{}); return .{ .shaped = .{ .shaped = shaped, .run = .{ .face = face, .size = size, .glyphs = shaped.placed.glyphs }, } }; }, } }
fn deinit(l: *Laid, gpa: Allocator) void { switch (l.*) { .layout => |*x| x.deinit(gpa), .shaped => |*x| x.shaped.deinit(gpa), } }
fn runs(l: *const Laid) []const renderer.Run { return switch (l.*) { .layout => |*x| x.runs, .shaped => |*x| @as(*const [1]renderer.Run, &x.run), }; }
fn endX(l: *const Laid) f32 { return switch (l.*) { .layout => |x| x.end_x, .shaped => |x| x.shaped.placed.end_x, }; }};
/// The smallest box holding every pixel with any coverage, or null if/// there are none.fn inkBox(sfc: *const z2d.Surface) ?Box { const w = sfc.getWidth(); const h = sfc.getHeight(); const pixels = sfc.image_surface_rgba.buf; var min_x = w; var min_y = h; var max_x: i32 = -1; var max_y: i32 = -1; var y: i32 = 0; while (y < h) : (y += 1) { var x: i32 = 0; while (x < w) : (x += 1) { if (pixels[@intCast(y * w + x)].a == 0) continue; min_x = @min(min_x, x); max_x = @max(max_x, x); min_y = @min(min_y, y); max_y = @max(max_y, y); } } if (max_x < 0) return null; return .{ .x = min_x, .y = min_y, .w = @intCast(max_x - min_x + 1), .h = @intCast(max_y - min_y + 1), };}
/// A new surface holding just `box` of `sfc`.fn crop(gpa: Allocator, sfc: *const z2d.Surface, box: Box) Error!z2d.Surface { const out = try z2d.Surface.init(.image_surface_rgba, gpa, @intCast(box.w), @intCast(box.h)); const src = sfc.image_surface_rgba.buf; const dst = out.image_surface_rgba.buf; const sw: usize = @intCast(sfc.getWidth()); for (0..box.h) |row| { const from = (@as(usize, @intCast(box.y)) + row) * sw + @as(usize, @intCast(box.x)); @memcpy(dst[row * box.w ..][0..box.w], src[from..][0..box.w]); } return out;}
const testing = std.testing;
test "inkBox and crop find and keep the ink" { const gpa = testing.allocator; var sfc = try z2d.Surface.init(.image_surface_rgba, gpa, 5, 4); defer sfc.deinit(gpa); try testing.expectEqual(@as(?Box, null), inkBox(&sfc)); const white: z2d.Pixel = .{ .rgba = .{ .r = 255, .g = 255, .b = 255, .a = 255 } }; sfc.putPixel(1, 1, white); sfc.putPixel(3, 2, white); const box = inkBox(&sfc).?; try testing.expectEqual(Box{ .x = 1, .y = 1, .w = 3, .h = 2 }, box); var out = try crop(gpa, &sfc, box); defer out.deinit(gpa); try testing.expectEqual(@as(i32, 3), out.getWidth()); try testing.expectEqual(@as(u8, 255), out.image_surface_rgba.buf[0].a); try testing.expectEqual(@as(u8, 0), out.image_surface_rgba.buf[1].a); try testing.expectEqual(@as(u8, 255), out.image_surface_rgba.buf[5].a);}