diff --git a/src/font/main.zig b/src/font/main.zig index a287d9a06..b9adf5cbf 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -12,6 +12,7 @@ pub const DeferredFace = @import("DeferredFace.zig"); pub const Face = face.Face; pub const Glyph = @import("Glyph.zig"); pub const Metrics = face.Metrics; +pub const opentype = @import("opentype.zig"); pub const shape = @import("shape.zig"); pub const Shaper = shape.Shaper; pub const ShaperCache = shape.Cache; diff --git a/src/font/opentype.zig b/src/font/opentype.zig new file mode 100644 index 000000000..798df5b2c --- /dev/null +++ b/src/font/opentype.zig @@ -0,0 +1,7 @@ +const svg = @import("opentype/svg.zig"); + +pub const SVG = svg.SVG; + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/src/font/opentype/svg.zig b/src/font/opentype/svg.zig new file mode 100644 index 000000000..4afffe7f1 --- /dev/null +++ b/src/font/opentype/svg.zig @@ -0,0 +1,108 @@ +const std = @import("std"); +const assert = std.debug.assert; +const font = @import("../main.zig"); + +/// SVG glyphs description table: +/// +/// References: +/// - https://www.w3.org/2013/10/SVG_in_OpenType/#thesvg +/// - https://learn.microsoft.com/en-us/typography/opentype/spec/svg +pub const SVG = struct { + /// The start and end glyph IDs (inclusive) that are present in the + /// table. This is used to very quickly include/exclude a glyph from + /// the table. + start_glyph_id: u16, + end_glyph_id: u16, + + /// All records in the table. + records: []const [12]u8, + + pub fn init(data: []const u8) !SVG { + var fbs = std.io.fixedBufferStream(data); + const reader = fbs.reader(); + + // Version + if (try reader.readInt(u16, .big) != 0) { + return error.SVGVersionNotSupported; + } + + // Offset + const offset = try reader.readInt(u32, .big); + + // Seek to the offset to get our document list + try fbs.seekTo(offset); + + // Get our document records along with the start/end glyph range. + const len = try reader.readInt(u16, .big); + const records: [*]const [12]u8 = @ptrCast(data[try fbs.getPos()..]); + const start_range = try glyphRange(&records[0]); + const end_range = if (len == 1) start_range else try glyphRange(&records[(len - 1)]); + + return .{ + .start_glyph_id = start_range[0], + .end_glyph_id = end_range[1], + .records = records[0..len], + }; + } + + pub fn hasGlyph(self: SVG, glyph_id: u16) bool { + // Fast path: outside the table range + if (glyph_id < self.start_glyph_id or glyph_id > self.end_glyph_id) { + return false; + } + + // Fast path, matches the start/end glyph IDs + if (glyph_id == self.start_glyph_id or glyph_id == self.end_glyph_id) { + return true; + } + + // Slow path: binary search our records + return std.sort.binarySearch( + [12]u8, + glyph_id, + self.records, + {}, + compareGlyphId, + ) != null; + } + + fn compareGlyphId(_: void, glyph_id: u16, record: [12]u8) std.math.Order { + const start, const end = glyphRange(&record) catch return .lt; + if (glyph_id < start) { + return .lt; + } else if (glyph_id > end) { + return .gt; + } else { + return .eq; + } + } + + fn glyphRange(record: []const u8) !struct { u16, u16 } { + var fbs = std.io.fixedBufferStream(record); + const reader = fbs.reader(); + return .{ + try reader.readInt(u16, .big), + try reader.readInt(u16, .big), + }; + } +}; + +test "SVG" { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = @import("../test.zig").fontJuliaMono; + + var lib = try font.Library.init(); + defer lib.deinit(); + + var face = try font.Face.init(lib, testFont, .{ .size = .{ .points = 12 } }); + defer face.deinit(); + + const table = (try face.copyTable(alloc, "SVG ")).?; + defer alloc.free(table); + + const svg = try SVG.init(table); + try testing.expectEqual(11482, svg.start_glyph_id); + try testing.expectEqual(11482, svg.end_glyph_id); + try testing.expect(svg.hasGlyph(11482)); +}