From 105e3c63d14fff1326efd4e84ae4b73a6e69e124 Mon Sep 17 00:00:00 2001 From: reugenio Date: Tue, 16 Dec 2025 13:25:17 +0100 Subject: [PATCH] fix: UTF-8 support in TTF drawText MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - drawText now decodes UTF-8 codepoints instead of iterating bytes - Fixes rendering of accented characters (á, é, í, ó, ú, ñ, etc.) - Uses std.unicode.utf8ByteSequenceLength + utf8Decode - Invalid UTF-8 sequences are gracefully skipped 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/render/ttf.zig | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/render/ttf.zig b/src/render/ttf.zig index fa0d571..5d53dfd 100644 --- a/src/render/ttf.zig +++ b/src/render/ttf.zig @@ -861,25 +861,41 @@ pub const TtfFont = struct { var cx = x; const baseline_y = y + self.ascent(); - for (text) |c| { - if (c == '\n') continue; - if (c == ' ') { - const metrics = self.getGlyphMetrics(c); + // UTF-8 iteration: decode codepoints instead of iterating bytes + var i: usize = 0; + while (i < text.len) { + // Get UTF-8 sequence length + const cp_len = std.unicode.utf8ByteSequenceLength(text[i]) catch { + i += 1; // Skip invalid byte + continue; + }; + if (i + cp_len > text.len) break; // Not enough bytes + + // Decode codepoint + const cp: u32 = std.unicode.utf8Decode(text[i..][0..cp_len]) catch { + i += cp_len; + continue; + }; + i += cp_len; + + if (cp == '\n') continue; + if (cp == ' ') { + const metrics = self.getGlyphMetrics(cp); cx += @intCast(metrics.advance); continue; } // Try to get cached glyph or rasterize - const cache_key = makeCacheKey(c, self.render_size); + const cache_key = makeCacheKey(cp, self.render_size); if (self.glyph_cache.get(cache_key)) |cached| { // Draw cached glyph self.drawGlyphBitmap(fb, cx, baseline_y, cached, color, clip); - const metrics = self.getGlyphMetrics(c); + const metrics = self.getGlyphMetrics(cp); cx += @intCast(metrics.advance); } else { // Rasterize and cache - const glyph_index = self.getGlyphIndex(c); + const glyph_index = self.getGlyphIndex(cp); if (self.getGlyphOutline(glyph_index)) |outline| { defer { var outline_copy = outline; @@ -895,9 +911,9 @@ pub const TtfFont = struct { .height = @intCast(bitmap.height), .bearing_x = @intCast(bitmap.bearing_x), .bearing_y = @intCast(bitmap.bearing_y), - .advance = self.getGlyphMetrics(c).advance, + .advance = self.getGlyphMetrics(cp).advance, }, - .codepoint = c, + .codepoint = cp, }; self.glyph_cache.put(cache_key, cached_glyph) catch {}; @@ -905,7 +921,7 @@ pub const TtfFont = struct { } } - const metrics = self.getGlyphMetrics(c); + const metrics = self.getGlyphMetrics(cp); cx += @intCast(metrics.advance); } }