//! TTF Font Support //! //! TrueType font loading and rendering support. //! Uses a simplified Zig implementation for basic TTF parsing. //! //! Features: //! - Load TTF files from memory or file //! - Rasterize glyphs at any size //! - Glyph caching for performance //! - Kerning support (basic) const std = @import("std"); const Allocator = std.mem.Allocator; const Style = @import("../core/style.zig"); const Layout = @import("../core/layout.zig"); const Framebuffer = @import("framebuffer.zig").Framebuffer; const Color = Style.Color; const Rect = Layout.Rect; // ============================================================================= // TTF Data Types // ============================================================================= /// Point on a glyph contour pub const GlyphPoint = struct { x: i16, y: i16, on_curve: bool, // true = on curve, false = control point (quadratic bezier) }; /// A single contour (closed path) pub const Contour = struct { points: []GlyphPoint, }; /// Glyph outline (all contours) pub const GlyphOutline = struct { contours: []Contour, x_min: i16, y_min: i16, x_max: i16, y_max: i16, allocator: Allocator, pub fn deinit(self: *GlyphOutline) void { for (self.contours) |contour| { self.allocator.free(contour.points); } self.allocator.free(self.contours); } }; /// Rasterized glyph bitmap pub const GlyphBitmap = struct { data: []u8, // Alpha values 0-255 width: u32, height: u32, bearing_x: i32, bearing_y: i32, allocator: Allocator, pub fn deinit(self: *GlyphBitmap) void { self.allocator.free(self.data); } }; // ============================================================================= // Rasterization // ============================================================================= /// Edge for scanline rasterization const Edge = struct { x0: f32, y0: f32, x1: f32, y1: f32, direction: i8, // +1 going up, -1 going down }; /// Rasterize a glyph outline to bitmap with antialiasing pub fn rasterizeGlyph( allocator: Allocator, outline: GlyphOutline, scale: f32, supersample: u8, // 1 = no AA, 2-4 = supersampling level ) ?GlyphBitmap { // Calculate scaled bounding box const x_min_f = @as(f32, @floatFromInt(outline.x_min)) * scale; const y_min_f = @as(f32, @floatFromInt(outline.y_min)) * scale; const x_max_f = @as(f32, @floatFromInt(outline.x_max)) * scale; const y_max_f = @as(f32, @floatFromInt(outline.y_max)) * scale; const width: u32 = @intFromFloat(@ceil(x_max_f - x_min_f) + 2); const height: u32 = @intFromFloat(@ceil(y_max_f - y_min_f) + 2); if (width == 0 or height == 0 or width > 1000 or height > 1000) return null; // Allocate bitmap const bitmap_size = width * height; var data = allocator.alloc(u8, bitmap_size) catch return null; @memset(data, 0); // Collect all edges from contours var edges_list: std.ArrayListUnmanaged(Edge) = .{}; defer edges_list.deinit(allocator); for (outline.contours) |contour| { collectEdgesFromContour(allocator, &edges_list, contour.points, scale, x_min_f, y_min_f, supersample) catch return null; } const ss = @as(f32, @floatFromInt(supersample)); const ss_sq = @as(f32, @floatFromInt(@as(u32, supersample) * @as(u32, supersample))); // Scanline fill with supersampling for (0..height) |py| { for (0..width) |px| { var coverage: u32 = 0; // Subsample for (0..supersample) |sy| { for (0..supersample) |sx| { const sample_x = @as(f32, @floatFromInt(px)) + (@as(f32, @floatFromInt(sx)) + 0.5) / ss; const sample_y = @as(f32, @floatFromInt(py)) + (@as(f32, @floatFromInt(sy)) + 0.5) / ss; // Count winding number var winding: i32 = 0; for (edges_list.items) |edge| { if (edgeCrossesRay(edge, sample_x, sample_y)) { winding += edge.direction; } } if (winding != 0) coverage += 1; } } // Convert coverage to alpha // Flip Y: TTF has Y-up, screen has Y-down const alpha: u8 = @intFromFloat((@as(f32, @floatFromInt(coverage)) / ss_sq) * 255.0); data[(height - 1 - py) * width + px] = alpha; } } return GlyphBitmap{ .data = data, .width = width, .height = height, .bearing_x = 0, // Bitmap is normalized to (0,0), no offset needed .bearing_y = @intFromFloat(y_max_f), // Distance from baseline to top of glyph .allocator = allocator, }; } /// Collect edges from a contour, handling bezier curves fn collectEdgesFromContour( allocator: Allocator, edges: *std.ArrayListUnmanaged(Edge), points: []const GlyphPoint, scale: f32, x_off: f32, y_off: f32, subdivisions: u8, ) !void { if (points.len < 2) return; const n = points.len; var i: usize = 0; while (i < n) { const p0 = points[i]; const p1 = points[(i + 1) % n]; const x0 = @as(f32, @floatFromInt(p0.x)) * scale - x_off; const y0 = @as(f32, @floatFromInt(p0.y)) * scale - y_off; const x1 = @as(f32, @floatFromInt(p1.x)) * scale - x_off; const y1 = @as(f32, @floatFromInt(p1.y)) * scale - y_off; if (p0.on_curve and p1.on_curve) { // Straight line try addEdge(allocator, edges, x0, y0, x1, y1); i += 1; } else if (p0.on_curve and !p1.on_curve) { // Bezier curve: p0 is on, p1 is control const p2 = points[(i + 2) % n]; var x2: f32 = undefined; var y2: f32 = undefined; if (p2.on_curve) { x2 = @as(f32, @floatFromInt(p2.x)) * scale - x_off; y2 = @as(f32, @floatFromInt(p2.y)) * scale - y_off; i += 2; } else { // Two off-curve points: interpolate midpoint x2 = (x1 + @as(f32, @floatFromInt(p2.x)) * scale - x_off) / 2.0; y2 = (y1 + @as(f32, @floatFromInt(p2.y)) * scale - y_off) / 2.0; i += 1; } // Subdivide bezier curve try subdivideBezier(allocator, edges, x0, y0, x1, y1, x2, y2, subdivisions * 2); } else { // Off-curve start: should have been handled, skip i += 1; } } } /// Subdivide quadratic bezier curve into line segments fn subdivideBezier( allocator: Allocator, edges: *std.ArrayListUnmanaged(Edge), x0: f32, y0: f32, cx: f32, cy: f32, x1: f32, y1: f32, steps: u8, ) !void { var prev_x = x0; var prev_y = y0; const step_f = @as(f32, @floatFromInt(steps)); for (1..steps + 1) |s| { const t = @as(f32, @floatFromInt(s)) / step_f; const t1 = 1.0 - t; // Quadratic bezier: B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2 const curr_x = t1 * t1 * x0 + 2.0 * t1 * t * cx + t * t * x1; const curr_y = t1 * t1 * y0 + 2.0 * t1 * t * cy + t * t * y1; try addEdge(allocator, edges, prev_x, prev_y, curr_x, curr_y); prev_x = curr_x; prev_y = curr_y; } } /// Add edge if it's not horizontal fn addEdge(allocator: Allocator, edges: *std.ArrayListUnmanaged(Edge), x0: f32, y0: f32, x1: f32, y1: f32) !void { // Skip horizontal edges if (@abs(y1 - y0) < 0.001) return; // Direction: +1 if going up (y increasing), -1 if going down const direction: i8 = if (y1 > y0) 1 else -1; // Always store with y0 < y1 if (y0 < y1) { try edges.append(allocator, Edge{ .x0 = x0, .y0 = y0, .x1 = x1, .y1 = y1, .direction = direction }); } else { try edges.append(allocator, Edge{ .x0 = x1, .y0 = y1, .x1 = x0, .y1 = y0, .direction = direction }); } } /// Check if edge crosses horizontal ray from point to the right fn edgeCrossesRay(edge: Edge, px: f32, py: f32) bool { // Ray goes from (px, py) to (+infinity, py) // Edge goes from (x0, y0) to (x1, y1) where y0 < y1 // Check if ray is within edge's y range if (py < edge.y0 or py >= edge.y1) return false; // Calculate x intersection const t = (py - edge.y0) / (edge.y1 - edge.y0); const x_intersect = edge.x0 + t * (edge.x1 - edge.x0); // Ray crosses if intersection is to the right of point return x_intersect > px; } /// TTF table directory entry const TableEntry = struct { tag: [4]u8, checksum: u32, offset: u32, length: u32, }; /// Glyph metrics pub const GlyphMetrics = struct { /// Width of the glyph bitmap width: u16 = 0, /// Height of the glyph bitmap height: u16 = 0, /// X bearing (left side bearing) bearing_x: i16 = 0, /// Y bearing (top side bearing from baseline) bearing_y: i16 = 0, /// Advance width to next character advance: u16 = 0, }; /// Cached glyph const CachedGlyph = struct { /// Bitmap data (alpha values 0-255) bitmap: []u8, /// Metrics metrics: GlyphMetrics, /// Character code codepoint: u32, }; /// Font metrics pub const FontMetrics = struct { /// Ascent (above baseline) ascent: i16 = 0, /// Descent (below baseline, typically negative) descent: i16 = 0, /// Line gap line_gap: i16 = 0, /// Units per em units_per_em: u16 = 2048, }; // ============================================================================= // TTF Font // ============================================================================= /// TrueType font pub const TtfFont = struct { allocator: Allocator, /// Raw font data data: []const u8, /// Whether we own the data owns_data: bool = false, /// Table offsets cmap_offset: u32 = 0, glyf_offset: u32 = 0, head_offset: u32 = 0, hhea_offset: u32 = 0, hmtx_offset: u32 = 0, loca_offset: u32 = 0, maxp_offset: u32 = 0, /// Font metrics metrics: FontMetrics = .{}, /// Number of glyphs num_glyphs: u16 = 0, /// Index to loc format (0 = short, 1 = long) index_to_loc_format: i16 = 0, /// Glyph cache (for rendered glyphs) glyph_cache: std.AutoHashMap(u64, CachedGlyph), /// Current render size render_size: u16 = 16, /// Scale factor for current size scale: f32 = 1.0, const Self = @This(); /// Load font from file pub fn loadFromFile(allocator: Allocator, path: []const u8) !Self { const file = try std.fs.cwd().openFile(path, .{}); defer file.close(); const stat = try file.stat(); const data = try allocator.alloc(u8, stat.size); const bytes_read = try file.readAll(data); if (bytes_read != stat.size) { allocator.free(data); return error.IncompleteRead; } var font = try initFromMemory(allocator, data); font.owns_data = true; return font; } /// Initialize font from memory pub fn initFromMemory(allocator: Allocator, data: []const u8) !Self { var self = Self{ .allocator = allocator, .data = data, .glyph_cache = std.AutoHashMap(u64, CachedGlyph).init(allocator), }; try self.parseHeader(); self.setSize(16); return self; } /// Initialize from embedded font (AdwaitaSans) /// Convenience function for zero external dependencies pub fn initEmbedded(allocator: Allocator) !Self { const embedded = @import("embedded_font.zig"); return initFromMemory(allocator, embedded.adwaita_sans_data); } /// Deinitialize font pub fn deinit(self: *Self) void { // Free cached glyphs var it = self.glyph_cache.iterator(); while (it.next()) |entry| { self.allocator.free(entry.value_ptr.bitmap); } self.glyph_cache.deinit(); // Free data if we own it if (self.owns_data) { self.allocator.free(@constCast(self.data)); } } /// Parse TTF header and locate tables fn parseHeader(self: *Self) !void { if (self.data.len < 12) return error.InvalidFont; // Check magic number (0x00010000 for TTF, 'true' for some Mac fonts) const magic = readU32Big(self.data, 0); if (magic != 0x00010000 and magic != 0x74727565) { return error.InvalidFont; } const num_tables = readU16Big(self.data, 4); // Parse table directory var offset: u32 = 12; var i: u16 = 0; while (i < num_tables) : (i += 1) { if (offset + 16 > self.data.len) return error.InvalidFont; const entry = TableEntry{ .tag = self.data[offset..][0..4].*, .checksum = readU32Big(self.data, offset + 4), .offset = readU32Big(self.data, offset + 8), .length = readU32Big(self.data, offset + 12), }; // Store table offsets if (std.mem.eql(u8, &entry.tag, "cmap")) self.cmap_offset = entry.offset; if (std.mem.eql(u8, &entry.tag, "glyf")) self.glyf_offset = entry.offset; if (std.mem.eql(u8, &entry.tag, "head")) self.head_offset = entry.offset; if (std.mem.eql(u8, &entry.tag, "hhea")) self.hhea_offset = entry.offset; if (std.mem.eql(u8, &entry.tag, "hmtx")) self.hmtx_offset = entry.offset; if (std.mem.eql(u8, &entry.tag, "loca")) self.loca_offset = entry.offset; if (std.mem.eql(u8, &entry.tag, "maxp")) self.maxp_offset = entry.offset; offset += 16; } // Parse head table if (self.head_offset > 0) { self.metrics.units_per_em = readU16Big(self.data, self.head_offset + 18); self.index_to_loc_format = @bitCast(readU16Big(self.data, self.head_offset + 50)); } // Parse hhea table if (self.hhea_offset > 0) { self.metrics.ascent = @bitCast(readU16Big(self.data, self.hhea_offset + 4)); self.metrics.descent = @bitCast(readU16Big(self.data, self.hhea_offset + 6)); self.metrics.line_gap = @bitCast(readU16Big(self.data, self.hhea_offset + 8)); } // Parse maxp table if (self.maxp_offset > 0) { self.num_glyphs = readU16Big(self.data, self.maxp_offset + 4); } } /// Set render size pub fn setSize(self: *Self, size: u16) void { self.render_size = size; self.scale = @as(f32, @floatFromInt(size)) / @as(f32, @floatFromInt(self.metrics.units_per_em)); } /// Get glyph index for codepoint pub fn getGlyphIndex(self: Self, codepoint: u32) u16 { if (self.cmap_offset == 0) return 0; // Parse cmap table to find glyph index const cmap_data = self.data[self.cmap_offset..]; if (cmap_data.len < 4) return 0; const num_subtables = readU16Big(cmap_data, 2); // Look for format 4 (Unicode BMP) or format 12 (Unicode full) var subtable_offset: u32 = 4; var i: u16 = 0; while (i < num_subtables) : (i += 1) { if (subtable_offset + 8 > cmap_data.len) break; const platform_id = readU16Big(cmap_data, subtable_offset); const encoding_id = readU16Big(cmap_data, subtable_offset + 2); const offset = readU32Big(cmap_data, subtable_offset + 4); // Unicode platform (0) or Windows platform (3) with Unicode encoding if ((platform_id == 0 or (platform_id == 3 and encoding_id == 1)) and offset < cmap_data.len) { const subtable = cmap_data[offset..]; const format = readU16Big(subtable, 0); if (format == 4 and codepoint < 0x10000) { return self.lookupFormat4(subtable, @intCast(codepoint)); } else if (format == 12) { return self.lookupFormat12(subtable, codepoint); } } subtable_offset += 8; } return 0; } /// Lookup glyph in format 4 subtable fn lookupFormat4(self: Self, subtable: []const u8, codepoint: u16) u16 { _ = self; if (subtable.len < 14) return 0; const seg_count_x2 = readU16Big(subtable, 6); const seg_count = seg_count_x2 / 2; const end_codes_offset: usize = 14; const start_codes_offset = end_codes_offset + seg_count_x2 + 2; // +2 for reserved pad const id_delta_offset = start_codes_offset + seg_count_x2; const id_range_offset_offset = id_delta_offset + seg_count_x2; // Binary search for segment var lo: u16 = 0; var hi = seg_count; while (lo < hi) { const mid = lo + (hi - lo) / 2; const end_code = readU16Big(subtable, end_codes_offset + @as(usize, mid) * 2); if (codepoint > end_code) { lo = mid + 1; } else { hi = mid; } } if (lo >= seg_count) return 0; const seg_idx: usize = lo; const end_code = readU16Big(subtable, end_codes_offset + seg_idx * 2); const start_code = readU16Big(subtable, start_codes_offset + seg_idx * 2); if (codepoint < start_code or codepoint > end_code) return 0; const id_delta: i16 = @bitCast(readU16Big(subtable, id_delta_offset + seg_idx * 2)); const id_range_offset = readU16Big(subtable, id_range_offset_offset + seg_idx * 2); if (id_range_offset == 0) { const result = @as(i32, codepoint) + @as(i32, id_delta); return @intCast(@as(u32, @bitCast(result)) & 0xFFFF); } else { const glyph_offset = id_range_offset_offset + seg_idx * 2 + id_range_offset + (@as(usize, codepoint) - @as(usize, start_code)) * 2; if (glyph_offset + 2 > subtable.len) return 0; const glyph_id = readU16Big(subtable, glyph_offset); if (glyph_id == 0) return 0; const result = @as(i32, glyph_id) + @as(i32, id_delta); return @intCast(@as(u32, @bitCast(result)) & 0xFFFF); } } /// Lookup glyph in format 12 subtable fn lookupFormat12(self: Self, subtable: []const u8, codepoint: u32) u16 { _ = self; if (subtable.len < 16) return 0; const num_groups = readU32Big(subtable, 12); var group_offset: usize = 16; var i: u32 = 0; while (i < num_groups) : (i += 1) { if (group_offset + 12 > subtable.len) break; const start_char = readU32Big(subtable, group_offset); const end_char = readU32Big(subtable, group_offset + 4); const start_glyph = readU32Big(subtable, group_offset + 8); if (codepoint >= start_char and codepoint <= end_char) { return @intCast(start_glyph + (codepoint - start_char)); } group_offset += 12; } return 0; } /// Get glyph location in glyf table fn getGlyphLocation(self: Self, glyph_index: u16) ?struct { offset: u32, length: u32 } { if (self.loca_offset == 0 or self.glyf_offset == 0) return null; if (glyph_index >= self.num_glyphs) return null; const loca_data = self.data[self.loca_offset..]; var offset1: u32 = undefined; var offset2: u32 = undefined; if (self.index_to_loc_format == 0) { // Short format (offsets divided by 2) if (@as(usize, glyph_index + 1) * 2 + 2 > loca_data.len) return null; offset1 = @as(u32, readU16Big(loca_data, @as(usize, glyph_index) * 2)) * 2; offset2 = @as(u32, readU16Big(loca_data, @as(usize, glyph_index + 1) * 2)) * 2; } else { // Long format if (@as(usize, glyph_index + 1) * 4 + 4 > loca_data.len) return null; offset1 = readU32Big(loca_data, @as(usize, glyph_index) * 4); offset2 = readU32Big(loca_data, @as(usize, glyph_index + 1) * 4); } if (offset1 == offset2) return null; // Empty glyph return .{ .offset = offset1, .length = offset2 - offset1, }; } /// Get glyph outline (contours with points) pub fn getGlyphOutline(self: Self, glyph_index: u16) ?GlyphOutline { const loc = self.getGlyphLocation(glyph_index) orelse return null; if (loc.length < 10) return null; const glyph_data = self.data[self.glyf_offset + loc.offset ..]; if (glyph_data.len < loc.length) return null; const num_contours: i16 = @bitCast(readU16Big(glyph_data, 0)); // Compound glyph (negative num_contours) - not supported yet if (num_contours < 0) return null; if (num_contours == 0) return null; const x_min: i16 = @bitCast(readU16Big(glyph_data, 2)); const y_min: i16 = @bitCast(readU16Big(glyph_data, 4)); const x_max: i16 = @bitCast(readU16Big(glyph_data, 6)); const y_max: i16 = @bitCast(readU16Big(glyph_data, 8)); const n_contours: usize = @intCast(num_contours); // Read endPtsOfContours var end_pts = self.allocator.alloc(u16, n_contours) catch return null; defer self.allocator.free(end_pts); var offset: usize = 10; for (0..n_contours) |i| { if (offset + 2 > glyph_data.len) return null; end_pts[i] = readU16Big(glyph_data, offset); offset += 2; } const n_points: usize = @as(usize, end_pts[n_contours - 1]) + 1; // Skip instructions if (offset + 2 > glyph_data.len) return null; const instruction_length = readU16Big(glyph_data, offset); offset += 2 + instruction_length; // Parse flags (with repeat encoding) var flags = self.allocator.alloc(u8, n_points) catch return null; defer self.allocator.free(flags); var point_idx: usize = 0; while (point_idx < n_points) { if (offset >= glyph_data.len) return null; const flag = glyph_data[offset]; offset += 1; flags[point_idx] = flag; point_idx += 1; // Repeat flag if ((flag & 0x08) != 0 and point_idx < n_points) { if (offset >= glyph_data.len) return null; const repeat_count = glyph_data[offset]; offset += 1; var r: usize = 0; while (r < repeat_count and point_idx < n_points) : (r += 1) { flags[point_idx] = flag; point_idx += 1; } } } // Parse X coordinates var x_coords = self.allocator.alloc(i16, n_points) catch return null; defer self.allocator.free(x_coords); var x: i16 = 0; for (0..n_points) |i| { const flag = flags[i]; const x_short = (flag & 0x02) != 0; const x_same_or_positive = (flag & 0x10) != 0; if (x_short) { if (offset >= glyph_data.len) return null; const dx: i16 = @intCast(glyph_data[offset]); offset += 1; x += if (x_same_or_positive) dx else -dx; } else if (!x_same_or_positive) { if (offset + 2 > glyph_data.len) return null; const dx: i16 = @bitCast(readU16Big(glyph_data, offset)); offset += 2; x += dx; } // else: x_same_or_positive and !x_short means same as previous x_coords[i] = x; } // Parse Y coordinates var y_coords = self.allocator.alloc(i16, n_points) catch return null; defer self.allocator.free(y_coords); var y: i16 = 0; for (0..n_points) |i| { const flag = flags[i]; const y_short = (flag & 0x04) != 0; const y_same_or_positive = (flag & 0x20) != 0; if (y_short) { if (offset >= glyph_data.len) return null; const dy: i16 = @intCast(glyph_data[offset]); offset += 1; y += if (y_same_or_positive) dy else -dy; } else if (!y_same_or_positive) { if (offset + 2 > glyph_data.len) return null; const dy: i16 = @bitCast(readU16Big(glyph_data, offset)); offset += 2; y += dy; } y_coords[i] = y; } // Build contours var contours = self.allocator.alloc(Contour, n_contours) catch return null; errdefer self.allocator.free(contours); var start_pt: usize = 0; for (0..n_contours) |c| { const end_pt: usize = @as(usize, end_pts[c]) + 1; const contour_len = end_pt - start_pt; var points = self.allocator.alloc(GlyphPoint, contour_len) catch { // Free already allocated contours for (0..c) |j| { self.allocator.free(contours[j].points); } self.allocator.free(contours); return null; }; for (0..contour_len) |i| { const pt_idx = start_pt + i; points[i] = GlyphPoint{ .x = x_coords[pt_idx], .y = y_coords[pt_idx], .on_curve = (flags[pt_idx] & 0x01) != 0, }; } contours[c] = Contour{ .points = points }; start_pt = end_pt; } return GlyphOutline{ .contours = contours, .x_min = x_min, .y_min = y_min, .x_max = x_max, .y_max = y_max, .allocator = self.allocator, }; } /// Get horizontal metrics for glyph pub fn getHMetrics(self: Self, glyph_index: u16) struct { advance: u16, lsb: i16 } { if (self.hmtx_offset == 0 or self.hhea_offset == 0) { return .{ .advance = @intFromFloat(@as(f32, @floatFromInt(self.render_size)) * 0.6), .lsb = 0 }; } const num_h_metrics = readU16Big(self.data, self.hhea_offset + 34); const hmtx_data = self.data[self.hmtx_offset..]; if (glyph_index < num_h_metrics) { const offset = @as(usize, glyph_index) * 4; if (offset + 4 > hmtx_data.len) { return .{ .advance = @intFromFloat(@as(f32, @floatFromInt(self.render_size)) * 0.6), .lsb = 0 }; } return .{ .advance = readU16Big(hmtx_data, offset), .lsb = @bitCast(readU16Big(hmtx_data, offset + 2)), }; } else { // Use last advance width const last_offset = @as(usize, num_h_metrics - 1) * 4; const lsb_offset = @as(usize, num_h_metrics) * 4 + (@as(usize, glyph_index) - num_h_metrics) * 2; if (last_offset + 4 > hmtx_data.len or lsb_offset + 2 > hmtx_data.len) { return .{ .advance = @intFromFloat(@as(f32, @floatFromInt(self.render_size)) * 0.6), .lsb = 0 }; } return .{ .advance = readU16Big(hmtx_data, last_offset), .lsb = @bitCast(readU16Big(hmtx_data, lsb_offset)), }; } } /// Get glyph metrics (scaled) pub fn getGlyphMetrics(self: Self, codepoint: u32) GlyphMetrics { const glyph_index = self.getGlyphIndex(codepoint); const h_metrics = self.getHMetrics(glyph_index); return GlyphMetrics{ .advance = @intFromFloat(@as(f32, @floatFromInt(h_metrics.advance)) * self.scale), .bearing_x = @intFromFloat(@as(f32, @floatFromInt(h_metrics.lsb)) * self.scale), }; } /// Get text width pub fn textWidth(self: Self, text: []const u8) u32 { var width: u32 = 0; for (text) |c| { const metrics = self.getGlyphMetrics(c); width += metrics.advance; } return width; } /// Get line height pub fn lineHeight(self: Self) u32 { const asc: f32 = @floatFromInt(self.metrics.ascent); const desc: f32 = @floatFromInt(self.metrics.descent); const gap: f32 = @floatFromInt(self.metrics.line_gap); return @intFromFloat((asc - desc + gap) * self.scale); } /// Get ascent (scaled) pub fn ascent(self: Self) i32 { return @intFromFloat(@as(f32, @floatFromInt(self.metrics.ascent)) * self.scale); } /// Get descent (scaled) pub fn descent(self: Self) i32 { return @intFromFloat(@as(f32, @floatFromInt(self.metrics.descent)) * self.scale); } /// Draw text using TTF font with real glyph rasterization pub fn drawText( self: *Self, fb: *Framebuffer, x: i32, y: i32, text: []const u8, color: Color, clip: Rect, ) void { var cx = x; const baseline_y = y + self.ascent(); for (text) |c| { if (c == '\n') continue; if (c == ' ') { const metrics = self.getGlyphMetrics(c); cx += @intCast(metrics.advance); continue; } // Try to get cached glyph or rasterize const cache_key = makeCacheKey(c, 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); cx += @intCast(metrics.advance); } else { // Rasterize and cache const glyph_index = self.getGlyphIndex(c); if (self.getGlyphOutline(glyph_index)) |outline| { defer { var outline_copy = outline; outline_copy.deinit(); } // Rasterize with 2x supersampling for antialiasing if (rasterizeGlyph(self.allocator, outline, self.scale, 2)) |bitmap| { const cached_glyph = CachedGlyph{ .bitmap = bitmap.data, .metrics = GlyphMetrics{ .width = @intCast(bitmap.width), .height = @intCast(bitmap.height), .bearing_x = @intCast(bitmap.bearing_x), .bearing_y = @intCast(bitmap.bearing_y), .advance = self.getGlyphMetrics(c).advance, }, .codepoint = c, }; self.glyph_cache.put(cache_key, cached_glyph) catch {}; self.drawGlyphBitmap(fb, cx, baseline_y, cached_glyph, color, clip); } } const metrics = self.getGlyphMetrics(c); cx += @intCast(metrics.advance); } } } /// Draw a cached glyph bitmap with alpha blending fn drawGlyphBitmap( self: Self, fb: *Framebuffer, x: i32, baseline_y: i32, glyph: CachedGlyph, color: Color, clip: Rect, ) void { _ = self; // Calculate position: bearing_y is distance from baseline to top const glyph_x = x + glyph.metrics.bearing_x; const glyph_y = baseline_y - glyph.metrics.bearing_y; const width = glyph.metrics.width; const height = glyph.metrics.height; // Draw each pixel with alpha blending for (0..height) |py| { for (0..width) |px| { const alpha = glyph.bitmap[py * width + px]; if (alpha == 0) continue; const screen_x = glyph_x + @as(i32, @intCast(px)); const screen_y = glyph_y + @as(i32, @intCast(py)); // Clip check if (screen_x < clip.x or screen_x >= clip.x + @as(i32, @intCast(clip.w))) continue; if (screen_y < clip.y or screen_y >= clip.y + @as(i32, @intCast(clip.h))) continue; if (screen_x < 0 or screen_y < 0) continue; if (screen_x >= @as(i32, @intCast(fb.width)) or screen_y >= @as(i32, @intCast(fb.height))) continue; // Alpha blend if (alpha == 255) { fb.setPixel(@intCast(screen_x), @intCast(screen_y), color); } else { // Get background pixel and convert u32 to Color (ABGR format) const bg_u32 = fb.getPixel(@intCast(screen_x), @intCast(screen_y)) orelse 0; const bg = Color{ .r = @truncate(bg_u32), .g = @truncate(bg_u32 >> 8), .b = @truncate(bg_u32 >> 16), .a = @truncate(bg_u32 >> 24), }; const blended = blendColors(color, bg, alpha); fb.setPixel(@intCast(screen_x), @intCast(screen_y), blended); } } } } }; /// Create cache key from codepoint and size fn makeCacheKey(codepoint: u32, size: u16) u64 { return (@as(u64, codepoint) << 16) | @as(u64, size); } /// Blend foreground color with background using alpha fn blendColors(fg: Color, bg: Color, alpha: u8) Color { const a = @as(u16, alpha); const inv_a = 255 - a; return Color{ .r = @intCast((@as(u16, fg.r) * a + @as(u16, bg.r) * inv_a) / 255), .g = @intCast((@as(u16, fg.g) * a + @as(u16, bg.g) * inv_a) / 255), .b = @intCast((@as(u16, fg.b) * a + @as(u16, bg.b) * inv_a) / 255), .a = 255, }; } // ============================================================================= // Helper functions // ============================================================================= fn readU16Big(data: []const u8, offset: usize) u16 { if (offset + 2 > data.len) return 0; return (@as(u16, data[offset]) << 8) | @as(u16, data[offset + 1]); } fn readU32Big(data: []const u8, offset: usize) u32 { if (offset + 4 > data.len) return 0; return (@as(u32, data[offset]) << 24) | (@as(u32, data[offset + 1]) << 16) | (@as(u32, data[offset + 2]) << 8) | @as(u32, data[offset + 3]); } // ============================================================================= // Font Interface - Unified API for both bitmap and TTF fonts // ============================================================================= /// Font type tag pub const FontType = enum { bitmap, ttf, }; /// Unified font reference pub const FontRef = union(FontType) { bitmap: *const @import("font.zig").Font, ttf: *TtfFont, pub fn textWidth(self: FontRef, text: []const u8) u32 { return switch (self) { .bitmap => |f| f.textWidth(text), .ttf => |f| f.textWidth(text), }; } pub fn charHeight(self: FontRef) u32 { return switch (self) { .bitmap => |f| f.charHeight(), .ttf => |f| f.lineHeight(), }; } pub fn drawText( self: FontRef, fb: *Framebuffer, x: i32, y: i32, text: []const u8, color: Color, clip: Rect, ) void { switch (self) { .bitmap => |f| f.drawText(fb, x, y, text, color, clip), .ttf => |f| @constCast(f).drawText(fb, x, y, text, color, clip), } } }; // ============================================================================= // Tests // ============================================================================= test "TTF types" { // Basic type tests const metrics = GlyphMetrics{ .width = 10, .height = 12, .bearing_x = 1, .bearing_y = 10, .advance = 8, }; try std.testing.expectEqual(@as(u16, 10), metrics.width); try std.testing.expectEqual(@as(u16, 8), metrics.advance); } test "FontRef bitmap" { const bitmap_font = @import("font.zig"); const font_ref = FontRef{ .bitmap = &bitmap_font.default_font }; try std.testing.expectEqual(@as(u32, 40), font_ref.textWidth("Hello")); try std.testing.expectEqual(@as(u32, 8), font_ref.charHeight()); } test "readU16Big" { const data = [_]u8{ 0x12, 0x34, 0x56, 0x78 }; try std.testing.expectEqual(@as(u16, 0x1234), readU16Big(&data, 0)); try std.testing.expectEqual(@as(u16, 0x3456), readU16Big(&data, 1)); } test "readU32Big" { const data = [_]u8{ 0x12, 0x34, 0x56, 0x78 }; try std.testing.expectEqual(@as(u32, 0x12345678), readU32Big(&data, 0)); } test "TTF load and rasterize AdwaitaSans" { const allocator = std.testing.allocator; // Try to load AdwaitaSans from system var font = TtfFont.loadFromFile(allocator, "/usr/share/fonts/adwaita-sans-fonts/AdwaitaSans-Regular.ttf") catch { // Font not available on this system, skip test return; }; defer font.deinit(); // Verify font was parsed correctly try std.testing.expect(font.num_glyphs > 0); try std.testing.expect(font.metrics.units_per_em > 0); // Get glyph index for 'A' const glyph_index = font.getGlyphIndex('A'); try std.testing.expect(glyph_index > 0); // Get glyph outline if (font.getGlyphOutline(glyph_index)) |outline| { var outline_mut = outline; defer outline_mut.deinit(); // 'A' should have contours (typically 2: outer and inner) try std.testing.expect(outline.contours.len > 0); // Rasterize font.setSize(24); if (rasterizeGlyph(allocator, outline, font.scale, 2)) |bitmap| { var bitmap_mut = bitmap; defer bitmap_mut.deinit(); // Should produce a bitmap with non-zero dimensions try std.testing.expect(bitmap.width > 0); try std.testing.expect(bitmap.height > 0); // Should have some non-zero alpha values (actual glyph data) var has_content = false; for (bitmap.data) |alpha| { if (alpha > 0) { has_content = true; break; } } try std.testing.expect(has_content); } } } test "TTF rasterize multiple characters" { const allocator = std.testing.allocator; var font = TtfFont.loadFromFile(allocator, "/usr/share/fonts/adwaita-sans-fonts/AdwaitaSans-Regular.ttf") catch { return; // Skip if font not available }; defer font.deinit(); font.setSize(16); // Test several characters const test_chars = "AaBb0123"; for (test_chars) |c| { const glyph_index = font.getGlyphIndex(c); if (glyph_index == 0) continue; if (font.getGlyphOutline(glyph_index)) |outline| { var outline_mut = outline; defer outline_mut.deinit(); if (rasterizeGlyph(allocator, outline, font.scale, 2)) |bitmap| { var bitmap_mut = bitmap; defer bitmap_mut.deinit(); try std.testing.expect(bitmap.width > 0); try std.testing.expect(bitmap.height > 0); } } } }