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.
13 kB · 288 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Embedded bitmap glyphs from `EBLC`/`EBDT`: monochrome or grayscale//! images drawn for a particular size, either by a bitmap-only font — a//! terminal font converted to `.otb`, say — or by an outline font that has//! hand-tuned images for its smallest sizes.//!//! A bitmap becomes an alpha mask like an outline does, so it is painted in//! the text color. At a strike's own size it is copied pixel for pixel, its//! origin rounded to a whole pixel; at any other size the nearest strike is//! resampled with a box filter, which keeps an integer enlargement crisp//! and blurs anything else as little as it can.//!//! `CBLC`/`CBDT` color bitmaps are PNGs, which this does not decode, and//! `EBSC`'s advice about which strike to scale is not taken: the nearest//! strike is the one used.
const std = @import("std");const Allocator = std.mem.Allocator;const font = @import("font");const z2d = @import("z2d");const bitmap = font.tables.bitmap;const Face = @import("Face.zig");const raster = @import("raster.zig");const resample = @import("resample.zig");
/// Which strikes `render` may draw from.pub const Match = enum { /// Only a strike at exactly the size asked for, which is what an /// outline font's bitmaps are for. exact, /// The nearest strike that has the glyph, scaled to the size asked for. nearest,};
/// How far a size may be from a strike's and still count as that strike's/// size: a sixty-fourth of a pixel, the precision of FreeType's sizes.const exact_tolerance = 1.0 / 64.0;
/// How deep composite bitmaps (image formats 8 and 9) may nest. Real fonts/// use one level, for an accent placed over a base letter.const max_depth = 4;
/// Glyph `glyph_id`'s embedded bitmap at `size` pixels per em as an alpha/// mask, or null if the face has no bitmap for it under `match` or the/// image will not decode.pub fn render( gpa: Allocator, face: *const Face, glyph_id: u16, size: f32, sub_x: f32, sub_y: f32, aa: z2d.options.AntiAliasMode, match: Match,) Allocator.Error!?raster.Image { if (!(size > 0) or !std.math.isFinite(size)) return null; const b = face.font.ebdt orelse return null; const strike = pick(b, glyph_id, size, match) orelse return null; const g = strike.glyph(glyph_id).?; const m = placement(g) orelse return null; if (m.width == 0 or m.height == 0) return null;
const alpha = try gpa.alloc(u8, @as(usize, m.width) * m.height); defer gpa.free(alpha); @memset(alpha, 0); if (!try decode(gpa, strike, g, m, alpha, 0)) return null;
const fx = size / @as(f32, @floatFromInt(@max(strike.ppem_x, 1))); const fy = size / @as(f32, @floatFromInt(@max(strike.ppem_y, 1))); const exact = @abs(size - @as(f32, @floatFromInt(strike.ppem_y))) <= exact_tolerance and @abs(size - @as(f32, @floatFromInt(strike.ppem_x))) <= exact_tolerance; // At its own size a bitmap is already aligned to the pixel grid it was // drawn for, and a quarter-pixel nudge would only smear it. const ox: f32 = if (exact) @round(sub_x) else sub_x; const oy: f32 = if (exact) @round(sub_y) else sub_y; const sx: f32 = if (exact) 1 else fx; const sy: f32 = if (exact) 1 else fy;
// The image's edges in pixels relative to the origin's pixel, y down. const x0 = @as(f32, @floatFromInt(m.bearing_x)) * sx + ox; const y0 = -@as(f32, @floatFromInt(m.bearing_y)) * sy + oy; const x1 = x0 + @as(f32, @floatFromInt(m.width)) * sx; const y1 = y0 + @as(f32, @floatFromInt(m.height)) * sy; const left: i32 = @intFromFloat(@floor(x0)); const top: i32 = @intFromFloat(@floor(y0)); const width: i32 = @as(i32, @intFromFloat(@ceil(x1))) - left; const height: i32 = @as(i32, @intFromFloat(@ceil(y1))) - top; if (width <= 0 or height <= 0 or width > raster.max_dimension or height > raster.max_dimension) return null;
var surface = z2d.Surface.init(.image_surface_alpha8, gpa, width, height) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return null, }; errdefer surface.deinit(gpa);
const pixels = surface.image_surface_alpha8.buf; try resample.image(gpa, alpha, m.width, m.height, 1, std.mem.sliceAsBytes(pixels), @intCast(width), @intCast(height), .{ .x = x0 - @as(f32, @floatFromInt(left)), .y = y0 - @as(f32, @floatFromInt(top)), .scale_x = sx, .scale_y = sy, }, .box); if (aa == .none) for (pixels) |*p| { p.a = if (p.a >= 128) 255 else 0; }; return .{ .surface = surface, .left = left, .top = top };}
/// Whether the face has any strike `render` can draw from.pub fn hasMasks(face: *const Face) bool { const b = face.font.ebdt orelse return false; for (b.strikes) |s| if (isMask(s)) return true; return false;}
fn isMask(s: bitmap.Strike) bool { return switch (s.bit_depth) { 1, 2, 4, 8 => true, else => false, };}
/// The strike to draw `glyph_id` from: exactly `size`, or failing that and/// allowed, the one nearest it that has the glyph, a larger one on a tie,/// as it scales down better than a smaller one scales up.fn pick(b: bitmap.Bitmaps, glyph_id: u16, size: f32, match: Match) ?bitmap.Strike { var best: ?bitmap.Strike = null; var best_diff: f32 = std.math.inf(f32); for (b.strikes) |s| { if (!isMask(s) or s.glyph(glyph_id) == null) continue; const ppem: f32 = @floatFromInt(s.ppem_y); const diff = @abs(ppem - size); if (match == .exact and diff > exact_tolerance) continue; if (diff < best_diff or (diff == best_diff and ppem > @as(f32, @floatFromInt(best.?.ppem_y)))) { best = s; best_diff = diff; } } return best;}
/// A glyph's box: its size in pixels, and where its top-left corner is/// relative to the origin, y up.const Placement = struct { width: u8, height: u8, bearing_x: i8, bearing_y: i8,};
fn placement(g: bitmap.Glyph) ?Placement { return switch (g.metrics orelse return null) { // Small metrics may be vertical ones in a strike that says so; a // font with vertical metrics alone is set vertically, which nothing // here does, so they are taken as horizontal either way. .small => |s| .{ .width = s.width, .height = s.height, .bearing_x = s.bearing_x, .bearing_y = s.bearing_y }, .big => |s| .{ .width = s.width, .height = s.height, .bearing_x = s.hori_bearing_x, .bearing_y = s.hori_bearing_y }, };}
/// Decode `g` into `out`, `m.width` by `m.height` alpha values, adding to/// what is there. False if the image is not one this can read, or is cut/// short.fn decode(gpa: Allocator, strike: bitmap.Strike, g: bitmap.Glyph, m: Placement, out: []u8, depth: usize) Allocator.Error!bool { const bd: usize = strike.bit_depth; const max: u32 = (@as(u32, 1) << @intCast(bd)) - 1; const w: usize = m.width; switch (g.kind()) { .png => return false, .byte_aligned, .bit_aligned => { const aligned = g.kind() == .byte_aligned; const stride_bits = if (aligned) (w * bd + 7) / 8 * 8 else w * bd; if (stride_bits * m.height > g.data.len * 8) return false; for (0..m.height) |y| for (0..w) |x| { const bit = y * stride_bits + x * bd; // Pixels are packed from the most significant bit, and at // these depths never straddle a byte. const byte = g.data[bit / 8]; const shift: u3 = @intCast(8 - bd - bit % 8); const v = (@as(u32, byte) >> shift) & max; const a: u8 = @intCast(v * 255 / max); const o = &out[y * w + x]; o.* = @max(o.*, a); }; return true; }, .composite => { if (depth >= max_depth) return false; for (g.components) |c| { const cg = strike.glyph(c.glyph_id) orelse continue; const cm = placement(cg) orelse continue; if (cm.width == 0 or cm.height == 0) continue; const piece = try gpa.alloc(u8, @as(usize, cm.width) * cm.height); defer gpa.free(piece); @memset(piece, 0); if (!try decode(gpa, strike, cg, cm, piece, depth + 1)) continue; // A component's offset is where its top-left corner goes in // the composite's image. for (0..cm.height) |py| for (0..cm.width) |px| { const x = @as(i32, c.x_offset) + @as(i32, @intCast(px)); const y = @as(i32, c.y_offset) + @as(i32, @intCast(py)); if (x < 0 or y < 0 or x >= m.width or y >= m.height) continue; const o = &out[@as(usize, @intCast(y)) * w + @as(usize, @intCast(x))]; o.* = @max(o.*, piece[py * cm.width + px]); }; } return true; }, }}
fn testStrike(bit_depth: u8, glyphs: []const bitmap.Glyph) bitmap.Strike { return .{ .hori = std.mem.zeroes(bitmap.LineMetrics), .vert = std.mem.zeroes(bitmap.LineMetrics), .color_ref = 0, .start_glyph_index = 0, .end_glyph_index = 0, .ppem_x = 8, .ppem_y = 8, .bit_depth = bit_depth, .flags = .{ .horizontal_metrics = true, .vertical_metrics = false, ._reserved = 0 }, .glyphs = glyphs, };}
fn small(w: u8, h: u8) bitmap.Metrics { return .{ .small = .{ .width = w, .height = h, .bearing_x = 0, .bearing_y = @intCast(h), .advance = w } };}
test "grayscale pixels are scaled to the full range of alpha" { // Format 2, bit-aligned, two bits a pixel: a three by two image whose // rows run on into each other, 00 01 10 / 11 00 11. const g: bitmap.Glyph = .{ .glyph_id = 1, .image_format = 2, .metrics = small(3, 2), .data = &.{ 0b00011011, 0b0011_0000 } }; const strike = testStrike(2, &.{g}); var out: [6]u8 = @splat(0); try std.testing.expect(try decode(std.testing.allocator, strike, g, placement(g).?, &out, 0)); try std.testing.expectEqualSlices(u8, &.{ 0, 85, 170, 255, 0, 255 }, &out);}
test "byte-aligned rows start on a byte" { // Format 1, one bit a pixel, three pixels a row padded to a byte. const g: bitmap.Glyph = .{ .glyph_id = 1, .image_format = 1, .metrics = small(3, 2), .data = &.{ 0b1010_0000, 0b0100_0000 } }; const strike = testStrike(1, &.{g}); var out: [6]u8 = @splat(0); try std.testing.expect(try decode(std.testing.allocator, strike, g, placement(g).?, &out, 0)); try std.testing.expectEqualSlices(u8, &.{ 255, 0, 255, 0, 255, 0 }, &out); // One row short is not an image. const short: bitmap.Glyph = .{ .glyph_id = 1, .image_format = 1, .metrics = small(3, 2), .data = &.{0xff} }; try std.testing.expect(!try decode(std.testing.allocator, strike, short, placement(short).?, &out, 0));}
test "a composite places its components at their offsets" { const dot: bitmap.Glyph = .{ .glyph_id = 1, .image_format = 1, .metrics = small(1, 1), .data = &.{0x80} }; const pair: bitmap.Glyph = .{ .glyph_id = 2, .image_format = 8, .metrics = small(3, 2), .data = &.{}, .components = &.{ .{ .glyph_id = 1, .x_offset = 0, .y_offset = 0 }, .{ .glyph_id = 1, .x_offset = 2, .y_offset = 1 }, // Outside the composite's box, and a glyph the strike lacks. .{ .glyph_id = 1, .x_offset = 5, .y_offset = 0 }, .{ .glyph_id = 9, .x_offset = 1, .y_offset = 0 }, }, }; const strike = testStrike(1, &.{ dot, pair }); var out: [6]u8 = @splat(0); try std.testing.expect(try decode(std.testing.allocator, strike, pair, placement(pair).?, &out, 0)); try std.testing.expectEqualSlices(u8, &.{ 255, 0, 0, 0, 0, 255 }, &out);}
test "a composite that contains itself stops" { const loop: bitmap.Glyph = .{ .glyph_id = 1, .image_format = 8, .metrics = small(1, 1), .data = &.{}, .components = &.{.{ .glyph_id = 1, .x_offset = 0, .y_offset = 0 }}, }; const strike = testStrike(1, &.{loop}); var out: [1]u8 = @splat(0); try std.testing.expect(try decode(std.testing.allocator, strike, loop, placement(loop).?, &out, 0)); try std.testing.expectEqual(@as(u8, 0), out[0]);}