Skip to documentation
SLOP

tiny.filigree.font.raster

Reference tiny.filigree font raster

Defined in font.

API (7)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callsfontraster
Static calls · unresolved targets: unknown · external targets: unknown.

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

Definitions2
Public names2
Members0
Version26.7.0
Revisiondaab053ee433