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.
9.5 kB · 207 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Outline glyphs: masks, placement, the cache, and variations.
const std = @import("std");const testing = std.testing;const font = @import("font");const renderer = @import("font_renderer");const z2d = @import("z2d");const common = @import("common.zig");const Fixture = common.Fixture;const fonts = common.fonts;
const black: z2d.Pixel = .{ .rgba = .{ .r = 0, .g = 0, .b = 0, .a = 255 } };
test "a mask covers the glyph's outline and no more" { var fx = try Fixture.init(testing.allocator, fonts.sans_ttf, .{}); defer fx.deinit(); const gid = try fx.glyph('H');
var img = (try renderer.raster.renderMask(testing.allocator, fx.face, gid, 40, 0, 0, .default)).?; defer img.deinit(testing.allocator); try testing.expect(!img.isColor());
const path = try fx.face.glyphOutline(testing.allocator, gid); defer testing.allocator.free(path.commands); const b = path.controlBounds().?; const s = fx.face.scale(40);
// The inked pixels, back in pixel coordinates relative to the glyph // origin, lie within a pixel of the outline's scaled box. An H is all // straight lines, so its control box is its real box. const box = common.inkBox(&img.surface).?; const left: f32 = @floatFromInt(box[0] + img.left); const right: f32 = @floatFromInt(box[2] + img.left); const top: f32 = @floatFromInt(box[1] + img.top); const bottom: f32 = @floatFromInt(box[3] + img.top); try testing.expectApproxEqAbs(b.x_min * s, left, 1); try testing.expectApproxEqAbs(b.x_max * s, right, 1); try testing.expectApproxEqAbs(-b.y_max * s, top, 1); try testing.expectApproxEqAbs(-b.y_min * s, bottom, 1);}
test "a space renders to nothing" { var fx = try Fixture.init(testing.allocator, fonts.sans_ttf, .{}); defer fx.deinit(); const img = try renderer.raster.render(testing.allocator, fx.face, try fx.glyph(' '), 40, 0, 0, .{}); try testing.expectEqual(@as(?renderer.Image, null), img);}
test "TrueType and CFF outlines of the same design draw the same ink" { var tt = try Fixture.init(testing.allocator, fonts.sans_ttf, .{}); defer tt.deinit(); var cff = try Fixture.init(testing.allocator, fonts.sans_otf, .{}); defer cff.deinit(); for ("Hdeor") |c| { var a = (try renderer.raster.renderMask(testing.allocator, tt.face, try tt.glyph(c), 48, 0, 0, .default)).?; defer a.deinit(testing.allocator); var b = (try renderer.raster.renderMask(testing.allocator, cff.face, try cff.glyph(c), 48, 0, 0, .default)).?; defer b.deinit(testing.allocator); const ia: f64 = @floatFromInt(common.ink(&a.surface)); const ib: f64 = @floatFromInt(common.ink(&b.surface)); try testing.expect(@abs(ia - ib) / ia < 0.03); }}
fn drawText(fx: *const Fixture, text: []const u8, cache: ?*renderer.GlyphCache, x: f32) !z2d.Surface { var surface = try z2d.Surface.init(.image_surface_rgba, testing.allocator, 200, 60); errdefer surface.deinit(testing.allocator); var placed = try renderer.simple_layout.layout(testing.allocator, fx.face, 32, text); defer placed.deinit(testing.allocator); try renderer.drawRun(testing.allocator, &surface, .{ .face = fx.face, .size = 32, .glyphs = placed.glyphs }, x, 40, .{ .color = black, .cache = cache, }); return surface;}
test "drawing through the cache gives the same pixels as without" { var fx = try Fixture.init(testing.allocator, fonts.sans_ttf, .{}); defer fx.deinit();
var plain = try drawText(&fx, "Hello, AVAWA!", null, 3.3); defer plain.deinit(testing.allocator); try testing.expect(common.ink(&plain) > 0);
var cache: renderer.GlyphCache = .init(testing.allocator, renderer.GlyphCache.default_budget); defer cache.deinit(); // Twice: once filling the cache, once drawing from it. for (0..2) |_| { var cached = try drawText(&fx, "Hello, AVAWA!", &cache, 3.3); defer cached.deinit(testing.allocator); try testing.expectEqualSlices(u8, common.bytesOf(&plain), common.bytesOf(&cached)); } try testing.expect(cache.count() > 0);
// A cache too small for more than a glyph or two evicts all the way // through and still draws the same. var tiny: renderer.GlyphCache = .init(testing.allocator, 2048); defer tiny.deinit(); var evicting = try drawText(&fx, "Hello, AVAWA!", &tiny, 3.3); defer evicting.deinit(testing.allocator); try testing.expectEqualSlices(u8, common.bytesOf(&plain), common.bytesOf(&evicting)); try testing.expect(tiny.used <= tiny.budget);
cache.evictFace(fx.face); try testing.expectEqual(@as(usize, 0), cache.count());}
test "a composited mask matches filling the outline directly" { var fx = try Fixture.init(testing.allocator, fonts.sans_ttf, .{}); defer fx.deinit(); var drawn = try drawText(&fx, "Hi floored 0123", null, 4); defer drawn.deinit(testing.allocator);
var placed = try renderer.simple_layout.layout(testing.allocator, fx.face, 32, "Hi floored 0123"); defer placed.deinit(testing.allocator); // Whole-pixel positions only, where the mask and the direct fill sample // exactly the same outline. for (placed.glyphs) |*g| g.x = @round(g.x); var drawn_whole = try z2d.Surface.init(.image_surface_rgba, testing.allocator, 200, 60); defer drawn_whole.deinit(testing.allocator); const r: renderer.Run = .{ .face = fx.face, .size = 32, .glyphs = placed.glyphs }; try renderer.drawRun(testing.allocator, &drawn_whole, r, 4, 40, .{ .color = black });
var path: z2d.Path = .empty; defer path.deinit(testing.allocator); try renderer.outline.appendRun(testing.allocator, &path, r, 4, 40); var filled = try z2d.Surface.init(.image_surface_rgba, testing.allocator, 200, 60); defer filled.deinit(testing.allocator); const pattern: z2d.Pattern = .{ .opaque_pattern = .{ .pixel = black } }; try z2d.painter.fill(testing.allocator, &filled, &pattern, path.nodes.items, .{});
// Overlapping glyphs would differ — a fill covers their union once, // compositing twice — and none of these overlap. var worst: u8 = 0; var y: i32 = 0; while (y < 60) : (y += 1) { var x: i32 = 0; while (x < 200) : (x += 1) { const a = common.alphaAt(&drawn_whole, x, y); const b = common.alphaAt(&filled, x, y); worst = @max(worst, if (a > b) a - b else b - a); } } try testing.expect(worst <= 2);}
test "glyphs are placed to the quarter pixel" { var fx = try Fixture.init(testing.allocator, fonts.sans_ttf, .{}); defer fx.deinit(); var a = try drawText(&fx, "l", null, 10); defer a.deinit(testing.allocator); var b = try drawText(&fx, "l", null, 10.5); defer b.deinit(testing.allocator); var c = try drawText(&fx, "l", null, 11); defer c.deinit(testing.allocator); // Half a pixel along is neither where it was nor a whole pixel on, and // its ink sits between the two. try testing.expect(!std.mem.eql(u8, common.bytesOf(&a), common.bytesOf(&b))); try testing.expect(!std.mem.eql(u8, common.bytesOf(&b), common.bytesOf(&c))); const ba = common.inkBox(&a).?; const bc = common.inkBox(&c).?; try testing.expectEqual(ba[0] + 1, bc[0]);}
test "a variable font draws heavier at a heavier weight" { inline for (.{ fonts.noto_variable_ttf, fonts.sans_variable_otf }) |data| { // 200 is as light as fixture-sans-variable goes, and is its default. var light = try Fixture.init(testing.allocator, data, .{ .variations = &.{.{ .tag = .init("wght"), .value = 200 }} }); defer light.deinit(); var heavy = try Fixture.init(testing.allocator, data, .{ .variations = &.{.{ .tag = .init("wght"), .value = 900 }} }); defer heavy.deinit(); try testing.expect(heavy.face.coords.len > 0); const gid = try light.glyph('o'); var a = (try renderer.raster.renderMask(testing.allocator, light.face, gid, 48, 0, 0, .default)).?; defer a.deinit(testing.allocator); var b = (try renderer.raster.renderMask(testing.allocator, heavy.face, gid, 48, 0, 0, .default)).?; defer b.deinit(testing.allocator); try testing.expect(common.ink(&b.surface) > common.ink(&a.surface) * 5 / 4); if (light.font.hvar != null) { try testing.expect(heavy.face.advance(gid) > light.face.advance(gid)); } }}
test "the default instance of a variable font has no coordinates" { var fx = try Fixture.init(testing.allocator, fonts.noto_variable_ttf, .{}); defer fx.deinit(); try testing.expectEqual(@as(usize, 0), fx.face.coords.len); // An axis the font does not have is ignored. var other = try Fixture.init(testing.allocator, fonts.noto_variable_ttf, .{ .variations = &.{.{ .tag = .init("XXXX"), .value = 5 }} }); defer other.deinit(); try testing.expectEqual(@as(usize, 0), other.face.coords.len);}
test "metrics come out in font units" { var fx = try Fixture.init(testing.allocator, fonts.sans_ttf, .{}); defer fx.deinit(); const m = fx.face.metrics(); try testing.expect(m.ascender > 0); try testing.expect(m.descender < 0); try testing.expect(m.lineHeight() >= m.ascender - m.descender); const px = m.scaled(fx.face.scale(20)); try testing.expectApproxEqAbs(m.ascender * 0.02, px.ascender, 1e-4);}