tiny.filigree.font.outline
Defined in font.
API (6)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
Source: lib/filigree/src/font/outline.zig
zig
const std = @import("std");const cff = @import("cff.zig");const face_table = @import("face.zig");pub const Error = error{ InvalidFont, MissingTable, UnsupportedOutline, OutOfMemory,};pub const Bounds = struct { x_min: i32 = 0, y_min: i32 = 0, x_max: i32 = 0, y_max: i32 = 0,};pub const Point = struct { x: i32, y: i32, on_curve: bool,};pub const Contour = struct { start: usize, end: usize,};pub const Glyph = struct { bounds: Bounds = .{}, contours: []Contour = &.{}, points: []Point = &.{}, pub fn deinit(self: Glyph, allocator: std.mem.Allocator) void { if (self.contours.len > 0) allocator.free(self.contours); if (self.points.len > 0) allocator.free(self.points); }};pub fn glyphAlloc( allocator: std.mem.Allocator, face: face_table.Face, glyph_id: u32,) Error!Glyph { if (glyph_id >= face.num_glyphs) return error.InvalidFont; if (face.tableSlice("glyf") == null or face.tableSlice("loca") == null) { if (face.tableSlice("CFF ") != null) return cffGlyphAlloc(allocator, face, glyph_id); } const head = face.tableSlice("head") orelse return error.MissingTable; const loca = face.tableSlice("loca") orelse return error.MissingTable; const glyf = face.tableSlice("glyf") orelse return error.MissingTable; if (head.len < 52) return error.InvalidFont; const loc_format = readI16(head, 50) catch return error.InvalidFont; return glyphAllocFromTables(allocator, loca, glyf, loc_format, glyph_id, 0);}fn cffGlyphAlloc( allocator: std.mem.Allocator, face: face_table.Face, glyph_id: u32,) Error!Glyph { var builder = CffGlyphBuilder.init(allocator); errdefer builder.deinit(); try cff.buildGlyph(face, glyph_id, &builder); return try builder.toGlyph();}const CffGlyphBuilder = struct { allocator: std.mem.Allocator, contours: std.ArrayListUnmanaged(Contour) = .empty, points: std.ArrayListUnmanaged(Point) = .empty, contour_start: ?usize = null, current_x: i32 = 0, current_y: i32 = 0, fn init(allocator: std.mem.Allocator) CffGlyphBuilder { return .{ .allocator = allocator }; } fn deinit(self: *CffGlyphBuilder) void { self.contours.deinit(self.allocator); self.points.deinit(self.allocator); } pub fn moveTo(self: *CffGlyphBuilder, x: i32, y: i32) Error!void { try self.close(); self.contour_start = self.points.items.len; try self.points.append(self.allocator, .{ .x = x, .y = y, .on_curve = true }); self.current_x = x; self.current_y = y; } pub fn lineTo(self: *CffGlyphBuilder, x: i32, y: i32) Error!void { try self.ensureContour(); try self.points.append(self.allocator, .{ .x = x, .y = y, .on_curve = true }); self.current_x = x; self.current_y = y; } pub fn curveTo(self: *CffGlyphBuilder, x1: i32, y1: i32, x2: i32, y2: i32, x3: i32, y3: i32) Error!void { try self.ensureContour(); const p0x: f32 = @floatFromInt(self.current_x); const p0y: f32 = @floatFromInt(self.current_y); const p1x: f32 = @floatFromInt(x1); const p1y: f32 = @floatFromInt(y1); const p2x: f32 = @floatFromInt(x2); const p2y: f32 = @floatFromInt(y2); const p3x: f32 = @floatFromInt(x3); const p3y: f32 = @floatFromInt(y3); const steps: usize = 12; for (1..(steps + 1)) |step| { const t = @as(f32, @floatFromInt(step)) / @as(f32, @floatFromInt(steps)); const mt = 1.0 - t; const x = mt * mt * mt * p0x + 3.0 * mt * mt * t * p1x + 3.0 * mt * t * t * p2x + t * t * t * p3x; const y = mt * mt * mt * p0y + 3.0 * mt * mt * t * p1y + 3.0 * mt * t * t * p2y + t * t * t * p3y; try self.points.append(self.allocator, .{ .x = roundPoint(x), .y = roundPoint(y), .on_curve = true }); } self.current_x = x3; self.current_y = y3; } pub fn close(self: *CffGlyphBuilder) Error!void { const start = self.contour_start orelse return; if (self.points.items.len - start >= 3) { try self.contours.append(self.allocator, .{ .start = start, .end = self.points.items.len }); } else { self.points.shrinkRetainingCapacity(start); } self.contour_start = null; } fn toGlyph(self: *CffGlyphBuilder) Error!Glyph { try self.close(); const contours = try self.contours.toOwnedSlice(self.allocator); errdefer self.allocator.free(contours); const points = try self.points.toOwnedSlice(self.allocator); errdefer self.allocator.free(points); return .{ .bounds = boundsFor(points), .contours = contours, .points = points, }; } fn ensureContour(self: *CffGlyphBuilder) Error!void { if (self.contour_start != null) return; try self.moveTo(self.current_x, self.current_y); }};fn boundsFor(points: []const Point) Bounds { if (points.len == 0) return .{}; var bounds = Bounds{ .x_min = points[0].x, .y_min = points[0].y, .x_max = points[0].x, .y_max = points[0].y, }; for (points[1..]) |point| { bounds.x_min = @min(bounds.x_min, point.x); bounds.y_min = @min(bounds.y_min, point.y); bounds.x_max = @max(bounds.x_max, point.x); bounds.y_max = @max(bounds.y_max, point.y); } return bounds;}fn roundPoint(value: f32) i32 { return @intFromFloat(@round(value));}const max_component_depth = 16;fn glyphAllocFromTables( allocator: std.mem.Allocator, loca: []const u8, glyf: []const u8, loc_format: i16, glyph_id: u32, depth: usize,) Error!Glyph { if (depth > max_component_depth) return error.InvalidFont; const range = try glyphRange(loca, glyph_id, loc_format); if (range.start == range.end) return .{}; if (range.start > range.end or range.end > glyf.len) return error.InvalidFont; return parseGlyphAlloc(allocator, loca, glyf, loc_format, glyf[range.start..range.end], depth);}const GlyphRange = struct { start: usize, end: usize,};fn glyphRange(loca: []const u8, glyph_id: u32, loc_format: i16) Error!GlyphRange { const index: usize = @intCast(glyph_id); if (loc_format == 0) { const offset = index * 2; if (offset + 4 > loca.len) return error.InvalidFont; return .{ .start = @as(usize, try readU16(loca, offset)) * 2, .end = @as(usize, try readU16(loca, offset + 2)) * 2, }; } if (loc_format == 1) { const offset = index * 4; if (offset + 8 > loca.len) return error.InvalidFont; return .{ .start = @intCast(try readU32(loca, offset)), .end = @intCast(try readU32(loca, offset + 4)), }; } return error.InvalidFont;}fn parseGlyphAlloc( allocator: std.mem.Allocator, loca: []const u8, glyf: []const u8, loc_format: i16, data: []const u8, depth: usize,) Error!Glyph { if (data.len < 10) return error.InvalidFont; const contour_count = try readI16(data, 0); const bounds = Bounds{ .x_min = try readI16(data, 2), .y_min = try readI16(data, 4), .x_max = try readI16(data, 6), .y_max = try readI16(data, 8), }; if (contour_count < 0) return parseCompositeGlyphAlloc(allocator, loca, glyf, loc_format, data, bounds, depth); if (contour_count == 0) return .{}; const contour_count_usize: usize = @intCast(contour_count); var offset: usize = 10; const contours = try allocator.alloc(Contour, contour_count_usize); errdefer allocator.free(contours); var point_count: usize = 0; for (0..contour_count_usize) |contour_index| { const end_point = try readU16(data, offset); offset += 2; const start = point_count; point_count = @as(usize, end_point) + 1; if (point_count < start) return error.InvalidFont; contours[contour_index] = .{ .start = start, .end = point_count }; } const instruction_len = try readU16(data, offset); offset += 2; if (offset + instruction_len > data.len) return error.InvalidFont; offset += instruction_len; const flags = try allocator.alloc(u8, point_count); defer allocator.free(flags); var flag_count: usize = 0; while (flag_count < point_count) { if (offset >= data.len) return error.InvalidFont; const flag = data[offset]; offset += 1; var repeat_count: usize = 1; if ((flag & 0x08) != 0) { if (offset >= data.len) return error.InvalidFont; repeat_count += data[offset]; offset += 1; } if (flag_count + repeat_count > point_count) return error.InvalidFont; @memset(flags[flag_count..][0..repeat_count], flag); flag_count += repeat_count; } const points = try allocator.alloc(Point, point_count); errdefer allocator.free(points); for (flags, points) |flag, *point| { point.* = .{ .x = 0, .y = 0, .on_curve = (flag & 0x01) != 0 }; } var x: i32 = 0; for (flags, points) |flag, *point| { const delta: i32 = if ((flag & 0x02) != 0) delta: { if (offset >= data.len) return error.InvalidFont; const value: i32 = data[offset]; offset += 1; break :delta if ((flag & 0x10) != 0) value else -value; } else if ((flag & 0x10) != 0) 0 else delta: { const value = try readI16(data, offset); offset += 2; break :delta value; }; x += delta; point.x = x; } var y: i32 = 0; for (flags, points) |flag, *point| { const delta: i32 = if ((flag & 0x04) != 0) delta: { if (offset >= data.len) return error.InvalidFont; const value: i32 = data[offset]; offset += 1; break :delta if ((flag & 0x20) != 0) value else -value; } else if ((flag & 0x20) != 0) 0 else delta: { const value = try readI16(data, offset); offset += 2; break :delta value; }; y += delta; point.y = y; } return .{ .bounds = bounds, .contours = contours, .points = points, };}const component_arg_words = 0x0001;const component_args_xy = 0x0002;const component_has_scale = 0x0008;const component_more = 0x0020;const component_has_xy_scale = 0x0040;const component_has_2x2 = 0x0080;const component_has_instructions = 0x0100;const ComponentTransform = struct { xx: f32 = 1, yx: f32 = 0, xy: f32 = 0, yy: f32 = 1, dx: i32 = 0, dy: i32 = 0, fn apply(self: ComponentTransform, point: Point) Point { const x: f32 = @floatFromInt(point.x); const y: f32 = @floatFromInt(point.y); return .{ .x = roundTransformed(self.xx * x + self.xy * y + @as(f32, @floatFromInt(self.dx))), .y = roundTransformed(self.yx * x + self.yy * y + @as(f32, @floatFromInt(self.dy))), .on_curve = point.on_curve, }; }};fn parseCompositeGlyphAlloc( allocator: std.mem.Allocator, loca: []const u8, glyf: []const u8, loc_format: i16, data: []const u8, bounds: Bounds, depth: usize,) Error!Glyph { var contours = std.ArrayListUnmanaged(Contour).empty; errdefer contours.deinit(allocator); var points = std.ArrayListUnmanaged(Point).empty; errdefer points.deinit(allocator); var offset: usize = 10; var flags: u16 = component_more; while ((flags & component_more) != 0) { if (offset + 4 > data.len) return error.InvalidFont; flags = try readU16(data, offset); const component_glyph_id = try readU16(data, offset + 2); offset += 4; var transform = ComponentTransform{}; if ((flags & component_arg_words) != 0) { if (offset + 4 > data.len) return error.InvalidFont; transform.dx = try readI16(data, offset); transform.dy = try readI16(data, offset + 2); offset += 4; } else { if (offset + 2 > data.len) return error.InvalidFont; transform.dx = try readI8(data, offset); transform.dy = try readI8(data, offset + 1); offset += 2; } if ((flags & component_args_xy) == 0) return error.UnsupportedOutline; if ((flags & component_has_scale) != 0) { if (offset + 2 > data.len) return error.InvalidFont; const scale = try readF2Dot14(data, offset); transform.xx = scale; transform.yy = scale; offset += 2; } else if ((flags & component_has_xy_scale) != 0) { if (offset + 4 > data.len) return error.InvalidFont; transform.xx = try readF2Dot14(data, offset); transform.yy = try readF2Dot14(data, offset + 2); offset += 4; } else if ((flags & component_has_2x2) != 0) { if (offset + 8 > data.len) return error.InvalidFont; transform.xx = try readF2Dot14(data, offset); transform.yx = try readF2Dot14(data, offset + 2); transform.xy = try readF2Dot14(data, offset + 4); transform.yy = try readF2Dot14(data, offset + 6); offset += 8; } var component = try glyphAllocFromTables(allocator, loca, glyf, loc_format, component_glyph_id, depth + 1); defer component.deinit(allocator); try appendComponent(allocator, &contours, &points, component, transform); } if ((flags & component_has_instructions) != 0) { const instruction_len = try readU16(data, offset); offset += 2; if (offset + instruction_len > data.len) return error.InvalidFont; } const owned_contours = try contours.toOwnedSlice(allocator); errdefer allocator.free(owned_contours); const owned_points = try points.toOwnedSlice(allocator); return .{ .bounds = bounds, .contours = owned_contours, .points = owned_points, };}fn appendComponent( allocator: std.mem.Allocator, contours: *std.ArrayListUnmanaged(Contour), points: *std.ArrayListUnmanaged(Point), component: Glyph, transform: ComponentTransform,) Error!void { const point_offset = points.items.len; try points.ensureUnusedCapacity(allocator, component.points.len); for (component.points) |point| { points.appendAssumeCapacity(transform.apply(point)); } try contours.ensureUnusedCapacity(allocator, component.contours.len); for (component.contours) |contour| { contours.appendAssumeCapacity(.{ .start = point_offset + contour.start, .end = point_offset + contour.end, }); }}fn readI8(data: []const u8, offset: usize) Error!i8 { if (offset >= data.len) return error.InvalidFont; return @bitCast(data[offset]);}fn readF2Dot14(data: []const u8, offset: usize) Error!f32 { const raw = try readI16(data, offset); return @as(f32, @floatFromInt(raw)) / 16384.0;}fn roundTransformed(value: f32) i32 { return @intFromFloat(@round(value));}fn readU16(data: []const u8, offset: usize) Error!u16 { if (offset + 2 > data.len) return error.InvalidFont; return std.mem.readInt(u16, data[offset..][0..2], .big);}fn readI16(data: []const u8, offset: usize) Error!i16 { return @bitCast(try readU16(data, offset));}fn readU32(data: []const u8, offset: usize) Error!u32 { if (offset + 4 > data.len) return error.InvalidFont; return std.mem.readInt(u32, data[offset..][0..4], .big);}test "outline loads simple fixture glyph" { 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 = try glyphAlloc(allocator, face, face.glyphId('A')); defer glyph.deinit(allocator); try std.testing.expectEqual(Bounds{ .x_min = 50, .y_min = 0, .x_max = 450, .y_max = 700 }, glyph.bounds); try std.testing.expectEqual(@as(usize, 1), glyph.contours.len); try std.testing.expectEqual(Contour{ .start = 0, .end = 4 }, glyph.contours[0]); try std.testing.expectEqual(@as(usize, 4), glyph.points.len); try std.testing.expectEqual(Point{ .x = 50, .y = 0, .on_curve = true }, glyph.points[0]); try std.testing.expectEqual(Point{ .x = 450, .y = 700, .on_curve = true }, glyph.points[2]);}test "outline reports empty fixture glyphs" { 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 = try glyphAlloc(allocator, face, face.glyphId('C')); defer glyph.deinit(allocator); try std.testing.expectEqual(@as(usize, 0), glyph.contours.len); try std.testing.expectEqual(@as(usize, 0), glyph.points.len);}test "outline loads composite fixture glyph" { 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 = try glyphAlloc(allocator, face, face.glyphId('i')); defer glyph.deinit(allocator); try std.testing.expectEqual(Bounds{ .x_min = 50, .y_min = 0, .x_max = 950, .y_max = 700 }, glyph.bounds); try std.testing.expectEqual(@as(usize, 2), glyph.contours.len); try std.testing.expectEqual(Contour{ .start = 0, .end = 4 }, glyph.contours[0]); try std.testing.expectEqual(Contour{ .start = 4, .end = 8 }, glyph.contours[1]); try std.testing.expectEqual(@as(usize, 8), glyph.points.len); try std.testing.expectEqual(Point{ .x = 550, .y = 0, .on_curve = true }, glyph.points[4]); try std.testing.expectEqual(Point{ .x = 950, .y = 700, .on_curve = true }, glyph.points[6]);}test "outline loads CFF fixture glyph" { 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 = try glyphAlloc(allocator, face, face.glyphId('A')); defer glyph.deinit(allocator); try std.testing.expectEqual(Bounds{ .x_min = 50, .y_min = 0, .x_max = 450, .y_max = 700 }, glyph.bounds); try std.testing.expectEqual(@as(usize, 1), glyph.contours.len); try std.testing.expectEqual(@as(usize, 4), glyph.points.len); try std.testing.expectEqual(Point{ .x = 50, .y = 0, .on_curve = true }, glyph.points[0]); try std.testing.expectEqual(Point{ .x = 450, .y = 700, .on_curve = true }, glyph.points[2]);}Source: lib/filigree/src/font/root.zig:16
zig
pub const outline = @import("outline.zig");Audit
| Definitions | 2 |
|---|---|
| Public names | 2 |
| Members | 4 |
| Version | 26.7.0 |
| Revision | daab053ee433 |