// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Text to glyphs without shaping: a stand-in until there is a shaper. //! //! Each codepoint becomes the glyph its `cmap` maps it to, placed one after //! another along the baseline by `hmtx` advances and legacy `kern` pairs. //! There is no `GSUB` and no `GPOS`, so no ligatures, no mark positioning, //! no contextual forms, and no right-to-left: Latin, Greek, Cyrillic and CJK //! come out readable, and scripts that need shaping do not. A shaping //! library replaces this by producing `Run`s itself. const std = @import("std"); const Allocator = std.mem.Allocator; const Face = @import("Face.zig"); const run = @import("run.zig"); pub const Layout = run.Placed; /// Lay `text` out in `face` at `size` pixels per em. A codepoint the face /// has no glyph for becomes glyph 0, `.notdef`, which is how a font draws /// a missing character; ill-formed UTF-8 becomes U+FFFD. The glyphs are /// owned by the caller. pub fn layout(gpa: Allocator, face: *const Face, size: f32, text: []const u8) Allocator.Error!Layout { var ids: std.ArrayList(u16) = .empty; defer ids.deinit(gpa); var it: Utf8Iterator = .{ .bytes = text }; while (it.next()) |cp| try ids.append(gpa, face.glyphIndex(cp) orelse 0); const positions = try gpa.alloc(run.Position, ids.items.len); defer gpa.free(positions); for (positions, ids.items, 0..) |*p, id, i| { p.* = .{ .x_advance = face.advance(id) }; if (i + 1 < ids.items.len) p.x_advance += face.kerning(id, ids.items[i + 1]); } return run.place(gpa, face, size, ids.items, positions); } /// Decodes UTF-8, giving U+FFFD for each maximal ill-formed subsequence /// as the Unicode standard recommends, rather than failing. pub const Utf8Iterator = struct { bytes: []const u8, i: usize = 0, pub fn next(it: *Utf8Iterator) ?u21 { if (it.i >= it.bytes.len) return null; const b0 = it.bytes[it.i]; const len = std.unicode.utf8ByteSequenceLength(b0) catch { it.i += 1; return 0xfffd; }; if (it.i + len > it.bytes.len) { it.i += validPrefix(it.bytes[it.i..]); return 0xfffd; } const seq = it.bytes[it.i..][0..len]; const cp = std.unicode.utf8Decode(seq) catch { it.i += validPrefix(seq); return 0xfffd; }; it.i += len; return cp; } /// How many bytes of a broken sequence to consume: the lead byte and /// the continuation bytes after it, at least one. fn validPrefix(s: []const u8) usize { var n: usize = 1; while (n < s.len and n < 4 and s[n] & 0xc0 == 0x80) n += 1; return n; } }; test "ill-formed UTF-8 becomes replacement characters" { var it: Utf8Iterator = .{ .bytes = "a\xffb\xe2\x82" }; try std.testing.expectEqual(@as(?u21, 'a'), it.next()); try std.testing.expectEqual(@as(?u21, 0xfffd), it.next()); try std.testing.expectEqual(@as(?u21, 'b'), it.next()); try std.testing.expectEqual(@as(?u21, 0xfffd), it.next()); try std.testing.expectEqual(@as(?u21, null), it.next()); }