fix: UTF-8 support in TTF drawText

- 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 <noreply@anthropic.com>
This commit is contained in:
reugenio 2025-12-16 13:25:17 +01:00
parent 222c4e1542
commit 105e3c63d1

View file

@ -861,25 +861,41 @@ pub const TtfFont = struct {
var cx = x; var cx = x;
const baseline_y = y + self.ascent(); const baseline_y = y + self.ascent();
for (text) |c| { // UTF-8 iteration: decode codepoints instead of iterating bytes
if (c == '\n') continue; var i: usize = 0;
if (c == ' ') { while (i < text.len) {
const metrics = self.getGlyphMetrics(c); // 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); cx += @intCast(metrics.advance);
continue; continue;
} }
// Try to get cached glyph or rasterize // 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| { if (self.glyph_cache.get(cache_key)) |cached| {
// Draw cached glyph // Draw cached glyph
self.drawGlyphBitmap(fb, cx, baseline_y, cached, color, clip); self.drawGlyphBitmap(fb, cx, baseline_y, cached, color, clip);
const metrics = self.getGlyphMetrics(c); const metrics = self.getGlyphMetrics(cp);
cx += @intCast(metrics.advance); cx += @intCast(metrics.advance);
} else { } else {
// Rasterize and cache // Rasterize and cache
const glyph_index = self.getGlyphIndex(c); const glyph_index = self.getGlyphIndex(cp);
if (self.getGlyphOutline(glyph_index)) |outline| { if (self.getGlyphOutline(glyph_index)) |outline| {
defer { defer {
var outline_copy = outline; var outline_copy = outline;
@ -895,9 +911,9 @@ pub const TtfFont = struct {
.height = @intCast(bitmap.height), .height = @intCast(bitmap.height),
.bearing_x = @intCast(bitmap.bearing_x), .bearing_x = @intCast(bitmap.bearing_x),
.bearing_y = @intCast(bitmap.bearing_y), .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 {}; 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); cx += @intCast(metrics.advance);
} }
} }