tiny.filigree.font.raster
Defined in font.
API (7)
Actions
Public operations.
glyphBitmapAlloc: Rasterizes one glyph.loadAtlasAlloc
Types and contracts
Public types and contracts.
Source
Source: lib/filigree/src/font/raster.zig
zig
const std = @import("std");const sys = @import("sys");const coverage = @import("coverage.zig");const face_table = @import("face.zig");const outline = @import("outline.zig");const cff_test_font_env = "FILIGREE_CFF_TEST_FONT";const max_test_font_bytes = 128 * 1024 * 1024;pub const Error = outline.Error || error{ GlyphIdTooLarge, InvalidAtlas, InvalidPixelSize,};pub const Rectangle = struct { x: f32 = 0, y: f32 = 0, width: f32 = 0, height: f32 = 0,};pub const GlyphBitmap = struct { codepoint: i32 = 0, glyph_id: u32 = 0, width: i32 = 0, height: i32 = 0, offset_x: i32 = 0, offset_y: i32 = 0, advance_x: i32 = 0, alpha: []u8 = &.{}, pub fn deinit(self: GlyphBitmap, allocator: std.mem.Allocator) void { if (self.alpha.len > 0) allocator.free(self.alpha); }};pub const AtlasGlyph = struct { codepoint: i32 = 0, glyph_id: u32 = 0, width: i32 = 0, height: i32 = 0, offset_x: i32 = 0, offset_y: i32 = 0, advance_x: i32 = 0,};pub const Atlas = struct { rgba: []u8 = &.{}, width: i32 = 0, height: i32 = 0, glyphs: []AtlasGlyph = &.{}, recs: []Rectangle = &.{}, base_size: i32 = 0, glyph_padding: i32 = 0, pub fn deinitPixels(self: *Atlas, allocator: std.mem.Allocator) void { if (self.rgba.len > 0) allocator.free(self.rgba); self.rgba = &.{}; } pub fn deinit(self: Atlas, allocator: std.mem.Allocator) void { if (self.rgba.len > 0) allocator.free(self.rgba); if (self.glyphs.len > 0) allocator.free(self.glyphs); if (self.recs.len > 0) allocator.free(self.recs); }};const FloatPoint = struct { x: f32, y: f32, on_curve: bool = true,};const ContourRange = struct { start: usize, end: usize,};const PixelBounds = struct { left: i32, top: i32, right: i32, bottom: i32, fn width(self: PixelBounds) i32 { return @max(0, self.right - self.left); } fn height(self: PixelBounds) i32 { return @max(0, self.bottom - self.top); }};/// Rasterizes one glyph. Scratch allocations end before return; `alpha` belongs to/// `output_allocator` and must be released with `GlyphBitmap.deinit` and the same allocator.pub fn glyphBitmapAlloc( output_allocator: std.mem.Allocator, scratch_allocator: std.mem.Allocator, face: face_table.Face, glyph_id: u32, pixel_size: i32,) Error!GlyphBitmap { if (pixel_size <= 0) return error.InvalidPixelSize; const scale = fontScale(face, pixel_size); const advance_x = scaleMetric(face.advanceWidth(glyph_id), scale); var glyph = try outline.glyphAlloc(scratch_allocator, face, glyph_id); defer glyph.deinit(scratch_allocator); if (glyph.points.len == 0 or glyph.bounds.x_max <= glyph.bounds.x_min or glyph.bounds.y_max <= glyph.bounds.y_min) { return blankGlyph(glyph_id, advance_x); } const pixel_bounds = glyphPixelBounds(glyph.bounds, face.ascender, scale); const width = pixel_bounds.width(); const height = pixel_bounds.height(); if (width <= 0 or height <= 0) return blankGlyph(glyph_id, advance_x); var points = std.ArrayListUnmanaged(FloatPoint).empty; defer points.deinit(scratch_allocator); var contours = std.ArrayListUnmanaged(ContourRange).empty; defer contours.deinit(scratch_allocator); try flattenGlyph(scratch_allocator, glyph, face.ascender, pixel_bounds, scale, &points, &contours); const alpha = try output_allocator.alloc(u8, try pixelCount(width, height)); errdefer output_allocator.free(alpha); @memset(alpha, 0); if (points.items.len > 0) { try coverage.rasterizeAlloc(scratch_allocator, alpha, width, height, points.items, contours.items); } return .{ .glyph_id = glyph_id, .width = width, .height = height, .offset_x = pixel_bounds.left, .offset_y = pixel_bounds.top, .advance_x = advance_x, .alpha = alpha, };}pub fn loadAtlasAlloc( output_allocator: std.mem.Allocator, scratch_allocator: std.mem.Allocator, font_bytes: []const u8, font_size: i32, glyph_ids: []const i32, codepoints: []const i32, padding: i32,) Error!Atlas { if (font_size <= 0) return error.InvalidPixelSize; if (padding < 0 or glyph_ids.len == 0 or glyph_ids.len != codepoints.len) return error.InvalidAtlas; const face = face_table.Face.init(font_bytes) catch return error.InvalidFont; if ((face.tableSlice("glyf") == null or face.tableSlice("loca") == null) and face.tableSlice("CFF ") == null) return error.UnsupportedOutline; const bitmaps = try scratch_allocator.alloc(GlyphBitmap, glyph_ids.len); var bitmap_count: usize = 0; errdefer { for (bitmaps[0..bitmap_count]) |bitmap| bitmap.deinit(scratch_allocator); scratch_allocator.free(bitmaps); } var glyph_scratch = std.heap.ArenaAllocator.init(scratch_allocator); defer glyph_scratch.deinit(); for (glyph_ids, codepoints) |raw_glyph_id, codepoint| { const glyph_id = std.math.cast(u32, raw_glyph_id) orelse return error.GlyphIdTooLarge; var bitmap = glyphBitmapAlloc(scratch_allocator, glyph_scratch.allocator(), face, glyph_id, font_size) catch |err| switch (err) { error.UnsupportedOutline => blankGlyph(glyph_id, scaleMetric(face.advanceWidth(glyph_id), fontScale(face, font_size))), else => return err, }; bitmap.codepoint = codepoint; bitmaps[bitmap_count] = bitmap; bitmap_count += 1; _ = glyph_scratch.reset(.retain_capacity); } const atlas = try packAtlasAlloc(output_allocator, bitmaps, font_size, padding); for (bitmaps) |bitmap| bitmap.deinit(scratch_allocator); scratch_allocator.free(bitmaps); return atlas;}fn blankGlyph(glyph_id: u32, advance_x: i32) GlyphBitmap { return .{ .glyph_id = glyph_id, .advance_x = advance_x, };}fn packAtlasAlloc( allocator: std.mem.Allocator, bitmaps: []const GlyphBitmap, font_size: i32, padding: i32,) Error!Atlas { const glyphs = try allocator.alloc(AtlasGlyph, bitmaps.len); errdefer allocator.free(glyphs); const recs = try allocator.alloc(Rectangle, bitmaps.len); errdefer allocator.free(recs); const target_width = chooseAtlasWidth(bitmaps, padding); var x: i32 = 0; var y: i32 = 0; var row_height: i32 = 0; var used_width: i32 = 0; for (bitmaps, 0..) |bitmap, index| { const packed_width = @max(1, bitmap.width + padding * 2); const packed_height = @max(1, bitmap.height + padding * 2); if (x > 0 and x + packed_width > target_width) { y += row_height; x = 0; row_height = 0; } recs[index] = .{ .x = @floatFromInt(x + padding), .y = @floatFromInt(y + padding), .width = @floatFromInt(bitmap.width), .height = @floatFromInt(bitmap.height), }; glyphs[index] = .{ .codepoint = bitmap.codepoint, .glyph_id = bitmap.glyph_id, .width = bitmap.width, .height = bitmap.height, .offset_x = bitmap.offset_x, .offset_y = bitmap.offset_y, .advance_x = bitmap.advance_x, }; x += packed_width; used_width = @max(used_width, x); row_height = @max(row_height, packed_height); } const width = @max(1, used_width); const height = @max(1, y + row_height); const rgba_len = std.math.mul(usize, try pixelCount(width, height), 4) catch return error.InvalidAtlas; const rgba = try allocator.alloc(u8, rgba_len); errdefer allocator.free(rgba); @memset(rgba, 0); for (bitmaps, recs) |bitmap, rec| { blitBitmap(rgba, width, height, bitmap, rec); } return .{ .rgba = rgba, .width = width, .height = height, .glyphs = glyphs, .recs = recs, .base_size = font_size, .glyph_padding = padding, };}fn chooseAtlasWidth(bitmaps: []const GlyphBitmap, padding: i32) i32 { var total_area: u64 = 0; var min_width: i32 = 1; for (bitmaps) |bitmap| { const packed_width = @max(1, bitmap.width + padding * 2); const packed_height = @max(1, bitmap.height + padding * 2); min_width = @max(min_width, packed_width); total_area += @as(u64, @intCast(packed_width)) * @as(u64, @intCast(packed_height)); } var width: i32 = 64; while (width < min_width) width *= 2; while (@as(u64, @intCast(width)) * @as(u64, @intCast(width)) < total_area and width < 2048) width *= 2; return width;}fn blitBitmap(rgba: []u8, atlas_width: i32, atlas_height: i32, bitmap: GlyphBitmap, rec: Rectangle) void { if (bitmap.width <= 0 or bitmap.height <= 0 or bitmap.alpha.len == 0) return; const dst_x0: i32 = @intFromFloat(rec.x); const dst_y0: i32 = @intFromFloat(rec.y); var row: i32 = 0; while (row < bitmap.height) : (row += 1) { var col: i32 = 0; while (col < bitmap.width) : (col += 1) { const dst_x = dst_x0 + col; const dst_y = dst_y0 + row; if (dst_x < 0 or dst_y < 0 or dst_x >= atlas_width or dst_y >= atlas_height) continue; const src_index = @as(usize, @intCast(row)) * @as(usize, @intCast(bitmap.width)) + @as(usize, @intCast(col)); const dst_index = (@as(usize, @intCast(dst_y)) * @as(usize, @intCast(atlas_width)) + @as(usize, @intCast(dst_x))) * 4; rgba[dst_index] = 255; rgba[dst_index + 1] = 255; rgba[dst_index + 2] = 255; rgba[dst_index + 3] = bitmap.alpha[src_index]; } }}fn flattenGlyph( allocator: std.mem.Allocator, glyph: outline.Glyph, ascender: i32, pixel_bounds: PixelBounds, scale: f32, points: *std.ArrayListUnmanaged(FloatPoint), contours: *std.ArrayListUnmanaged(ContourRange),) Error!void { for (glyph.contours) |contour| { if (contour.start >= contour.end or contour.end > glyph.points.len) return error.InvalidFont; try flattenContour( allocator, glyph.points[contour.start..contour.end], ascender, pixel_bounds, scale, points, contours, ); }}fn flattenContour( allocator: std.mem.Allocator, raw: []const outline.Point, ascender: i32, pixel_bounds: PixelBounds, scale: f32, points: *std.ArrayListUnmanaged(FloatPoint), contours: *std.ArrayListUnmanaged(ContourRange),) Error!void { if (raw.len == 0) return; var expanded = std.ArrayListUnmanaged(FloatPoint).empty; defer expanded.deinit(allocator); for (raw, 0..) |point, index| { const next = raw[(index + 1) % raw.len]; const converted = convertPoint(point, ascender, pixel_bounds, scale); try expanded.append(allocator, converted); if (!point.on_curve and !next.on_curve) { try expanded.append(allocator, midpoint(converted, convertPoint(next, ascender, pixel_bounds, scale))); } } const start_index = firstOnCurve(expanded.items) orelse return; const range_start = points.items.len; var current = expanded.items[start_index]; current.on_curve = true; try points.append(allocator, current); var relative: usize = 1; while (relative < expanded.items.len) { const next = expanded.items[(start_index + relative) % expanded.items.len]; if (next.on_curve) { try points.append(allocator, next); current = next; relative += 1; continue; } const end_index = (start_index + relative + 1) % expanded.items.len; const end = expanded.items[end_index]; if (!end.on_curve) return error.InvalidFont; try appendQuadratic(allocator, points, current, next, end); current = end; relative += 2; } if (points.items.len - range_start >= 3) { try contours.append(allocator, .{ .start = range_start, .end = points.items.len }); } else { points.shrinkRetainingCapacity(range_start); }}fn convertPoint(point: outline.Point, ascender: i32, pixel_bounds: PixelBounds, scale: f32) FloatPoint { return .{ .x = @as(f32, @floatFromInt(point.x)) * scale - @as(f32, @floatFromInt(pixel_bounds.left)), .y = @as(f32, @floatFromInt(ascender - point.y)) * scale - @as(f32, @floatFromInt(pixel_bounds.top)), .on_curve = point.on_curve, };}fn midpoint(a: FloatPoint, b: FloatPoint) FloatPoint { return .{ .x = (a.x + b.x) * 0.5, .y = (a.y + b.y) * 0.5, .on_curve = true, };}fn firstOnCurve(points: []const FloatPoint) ?usize { for (points, 0..) |point, index| { if (point.on_curve) return index; } return null;}fn appendQuadratic( allocator: std.mem.Allocator, points: *std.ArrayListUnmanaged(FloatPoint), p0: FloatPoint, p1: FloatPoint, p2: FloatPoint,) Error!void { const steps: usize = 8; for (1..(steps + 1)) |step| { const t = @as(f32, @floatFromInt(step)) / @as(f32, @floatFromInt(steps)); const mt = 1.0 - t; try points.append(allocator, .{ .x = mt * mt * p0.x + 2.0 * mt * t * p1.x + t * t * p2.x, .y = mt * mt * p0.y + 2.0 * mt * t * p1.y + t * t * p2.y, }); }}fn fontScale(face: face_table.Face, pixel_size: i32) f32 { return @as(f32, @floatFromInt(pixel_size)) / @as(f32, @floatFromInt(face.units_per_em));}fn glyphPixelBounds(bounds: outline.Bounds, ascender: i32, scale: f32) PixelBounds { return .{ .left = scaleFloor(bounds.x_min, scale), .top = scaleFloor(ascender - bounds.y_max, scale), .right = scaleCeil(bounds.x_max, scale), .bottom = scaleCeil(ascender - bounds.y_min, scale), };}fn scaleMetric(value: u16, scale: f32) i32 { return scaleRound(value, scale);}fn scaleFloor(value: i32, scale: f32) i32 { return @intFromFloat(@floor(@as(f32, @floatFromInt(value)) * scale));}fn scaleCeil(value: i32, scale: f32) i32 { return @intFromFloat(@ceil(@as(f32, @floatFromInt(value)) * scale));}fn scaleRound(value: anytype, scale: f32) i32 { return @intFromFloat(@round(@as(f32, @floatFromInt(value)) * scale));}fn pixelCount(width: i32, height: i32) Error!usize { if (width <= 0 or height <= 0) return error.InvalidAtlas; return std.math.mul(usize, @intCast(width), @intCast(height)) catch error.InvalidAtlas;}fn hasVisiblePixel(bytes: []const u8) bool { for (bytes) |byte| { if (byte != 0) return true; } return false;}fn expectGlyphCoverageMatchesReference( allocator: std.mem.Allocator, face: face_table.Face, glyph_id: u32, pixel_size: i32,) !void { const scale = fontScale(face, pixel_size); var glyph = try outline.glyphAlloc(allocator, face, glyph_id); defer glyph.deinit(allocator); if (glyph.points.len == 0 or glyph.bounds.x_max <= glyph.bounds.x_min or glyph.bounds.y_max <= glyph.bounds.y_min) return; const pixel_bounds = glyphPixelBounds(glyph.bounds, face.ascender, scale); const width = pixel_bounds.width(); const height = pixel_bounds.height(); if (width <= 0 or height <= 0) return; var points = std.ArrayListUnmanaged(FloatPoint).empty; defer points.deinit(allocator); var contours = std.ArrayListUnmanaged(ContourRange).empty; defer contours.deinit(allocator); try flattenGlyph(allocator, glyph, face.ascender, pixel_bounds, scale, &points, &contours); const expected = try allocator.alloc(u8, try pixelCount(width, height)); defer allocator.free(expected); coverage.rasterizeReference(expected, width, height, points.items, contours.items); const bitmap = try glyphBitmapAlloc(allocator, allocator, face, glyph_id, pixel_size); defer bitmap.deinit(allocator); try std.testing.expectEqualSlices(u8, expected, bitmap.alpha);}test "scanline rasterization matches reference fixture outlines" { const fixtures = @import("../fixture/root.zig"); const allocator = std.testing.allocator; const true_type_bytes = try fixtures.createWithOutlines(allocator); defer allocator.free(true_type_bytes); const true_type_face = try face_table.Face.init(true_type_bytes); for ([_]i32{ 13, 20, 32 }) |pixel_size| { for ([_]u21{ 'A', 'B', 'i' }) |codepoint| { try expectGlyphCoverageMatchesReference(allocator, true_type_face, true_type_face.glyphId(codepoint), pixel_size); } } const cff_bytes = try fixtures.createWithCffOutlines(allocator); defer allocator.free(cff_bytes); const cff_face = try face_table.Face.init(cff_bytes); for ([_]i32{ 13, 20, 32 }) |pixel_size| { try expectGlyphCoverageMatchesReference(allocator, cff_face, cff_face.glyphId('A'), pixel_size); }}test "raster renders fixture outline alpha" { const fixtures = @import("../fixture/root.zig"); const allocator = std.testing.allocator; const bytes = try fixtures.createWithOutlines(allocator); defer allocator.free(bytes); const face = try face_table.Face.init(bytes); const glyph_id = face.glyphId('A'); const bitmap = try glyphBitmapAlloc(allocator, allocator, face, glyph_id, 20); defer bitmap.deinit(allocator); try std.testing.expect(bitmap.width > 0); try std.testing.expect(bitmap.height > 0); try std.testing.expectEqual(@as(i32, 10), bitmap.advance_x); try std.testing.expect(hasVisiblePixel(bitmap.alpha));}test "raster snaps glyph bitmap bounds from shared pixel edges" { const bounds = outline.Bounds{ .x_min = 25, .y_min = 1, .x_max = 626, .y_max = 700 }; const pixel_bounds = glyphPixelBounds(bounds, 1000, 0.016); try std.testing.expectEqual(@as(i32, 0), pixel_bounds.left); try std.testing.expectEqual(@as(i32, 4), pixel_bounds.top); try std.testing.expectEqual(@as(i32, 11), pixel_bounds.right); try std.testing.expectEqual(@as(i32, 16), pixel_bounds.bottom); try std.testing.expectEqual(@as(i32, 11), pixel_bounds.width()); try std.testing.expectEqual(@as(i32, 12), pixel_bounds.height()); const top_left = convertPoint(.{ .x = 25, .y = 700, .on_curve = true }, 1000, pixel_bounds, 0.016); const bottom_right = convertPoint(.{ .x = 626, .y = 1, .on_curve = true }, 1000, pixel_bounds, 0.016); try std.testing.expectApproxEqAbs(@as(f32, 0.4), top_left.x, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 0.8), top_left.y, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 10.016), bottom_right.x, 0.001); try std.testing.expectApproxEqAbs(@as(f32, 11.984), bottom_right.y, 0.001);}test "raster renders composite fixture outline alpha" { const fixtures = @import("../fixture/root.zig"); const allocator = std.testing.allocator; const bytes = try fixtures.createWithOutlines(allocator); defer allocator.free(bytes); const face = try face_table.Face.init(bytes); const bitmap = try glyphBitmapAlloc(allocator, allocator, face, face.glyphId('i'), 20); defer bitmap.deinit(allocator); try std.testing.expectEqual(@as(i32, 18), bitmap.width); try std.testing.expectEqual(@as(i32, 14), bitmap.height); try std.testing.expectEqual(@as(i32, 1), bitmap.offset_x); try std.testing.expectEqual(@as(i32, 2), bitmap.offset_y); try std.testing.expectEqual(@as(i32, 10), bitmap.advance_x); try std.testing.expectEqual(@as(usize, 252), bitmap.alpha.len); var row: usize = 0; while (row < 14) : (row += 1) { var col: usize = 0; while (col < 18) : (col += 1) { const expected: u8 = if (col < 8 or col >= 10) 255 else 0; try std.testing.expectEqual(expected, bitmap.alpha[row * 18 + col]); } }}test "atlas packs fixture glyph bitmaps as rgba" { const fixtures = @import("../fixture/root.zig"); const allocator = std.testing.allocator; const bytes = try fixtures.createWithOutlines(allocator); defer allocator.free(bytes); const face = try face_table.Face.init(bytes); var glyph_ids = [_]i32{ @intCast(face.glyphId('A')), @intCast(face.glyphId('B')), @intCast(face.glyphId(' ')), }; const codepoints = [_]i32{ 'A', 'B', ' ' }; const atlas = try loadAtlasAlloc(allocator, allocator, bytes, 20, &glyph_ids, &codepoints, 2); defer atlas.deinit(allocator); try std.testing.expectEqual(@as(usize, 3), atlas.glyphs.len); try std.testing.expectEqual(@as(i32, 'A'), atlas.glyphs[0].codepoint); try std.testing.expect(atlas.width > 0); try std.testing.expect(atlas.height > 0); try std.testing.expect(atlas.rgba.len >= 4); try std.testing.expect(hasVisiblePixel(atlas.rgba));}test "raster renders CFF fixture outline alpha" { const fixtures = @import("../fixture/root.zig"); const allocator = std.testing.allocator; const bytes = try fixtures.createWithCffOutlines(allocator); defer allocator.free(bytes); const face = try face_table.Face.init(bytes); const glyph_id = face.glyphId('A'); const bitmap = try glyphBitmapAlloc(allocator, allocator, face, glyph_id, 20); defer bitmap.deinit(allocator); try std.testing.expect(bitmap.width > 0); try std.testing.expect(bitmap.height > 0); try std.testing.expectEqual(@as(i32, 10), bitmap.advance_x); try std.testing.expect(hasVisiblePixel(bitmap.alpha));}test "raster renders configured CFF font alpha" { const allocator = std.testing.allocator; const path = sys.env.getOwned(allocator, cff_test_font_env) catch null orelse return error.SkipZigTest; defer allocator.free(path); const bytes = try sys.fs.readFileAlloc(allocator, path, max_test_font_bytes); defer allocator.free(bytes); const face = try face_table.Face.init(bytes); if (face.tableSlice("CFF ") == null) return error.SkipZigTest; const glyph_id = face.glyphId('A'); if (glyph_id == 0) return error.SkipZigTest; const bitmap = try glyphBitmapAlloc(allocator, allocator, face, glyph_id, 32); defer bitmap.deinit(allocator); try std.testing.expect(bitmap.width > 0); try std.testing.expect(bitmap.height > 0); try std.testing.expect(hasVisiblePixel(bitmap.alpha));}Source: lib/filigree/src/font/root.zig:17
zig
pub const raster = @import("raster.zig");Audit
| Definitions | 2 |
|---|---|
| Public names | 2 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |