From 7e5a164be82889a84d14d5fe7b30b5ae5af95e10 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sat, 28 Sep 2024 22:01:31 -0600 Subject: [PATCH] font/opentype: add table parsing for head, hhea, post, and OS/2 (and a utility for parsing SFNT font files and fetching table data from them) --- src/font/opentype.zig | 10 + src/font/opentype/head.zig | 179 ++++++++++++ src/font/opentype/hhea.zig | 115 ++++++++ src/font/opentype/os2.zig | 577 +++++++++++++++++++++++++++++++++++++ src/font/opentype/post.zig | 82 ++++++ src/font/opentype/sfnt.zig | 314 ++++++++++++++++++++ 6 files changed, 1277 insertions(+) create mode 100644 src/font/opentype/head.zig create mode 100644 src/font/opentype/hhea.zig create mode 100644 src/font/opentype/os2.zig create mode 100644 src/font/opentype/post.zig create mode 100644 src/font/opentype/sfnt.zig diff --git a/src/font/opentype.zig b/src/font/opentype.zig index 798df5b2c..dd02efeb3 100644 --- a/src/font/opentype.zig +++ b/src/font/opentype.zig @@ -1,6 +1,16 @@ +pub const sfnt = @import("opentype/sfnt.zig"); + const svg = @import("opentype/svg.zig"); +const os2 = @import("opentype/os2.zig"); +const post = @import("opentype/post.zig"); +const hhea = @import("opentype/hhea.zig"); +const head = @import("opentype/head.zig"); pub const SVG = svg.SVG; +pub const OS2 = os2.OS2; +pub const Post = post.Post; +pub const Hhea = hhea.Hhea; +pub const Head = head.Head; test { @import("std").testing.refAllDecls(@This()); diff --git a/src/font/opentype/head.zig b/src/font/opentype/head.zig new file mode 100644 index 000000000..ff5da7eaa --- /dev/null +++ b/src/font/opentype/head.zig @@ -0,0 +1,179 @@ +const std = @import("std"); +const assert = std.debug.assert; +const sfnt = @import("sfnt.zig"); + +/// Font Header Table +/// +/// References: +/// - https://learn.microsoft.com/en-us/typography/opentype/spec/head +pub const Head = extern struct { + /// Major version number of the font header table — set to 1. + majorVersion: sfnt.uint16 align(1), + + /// Minor version number of the font header table — set to 0. + minorVersion: sfnt.uint16 align(1), + + /// Set by font manufacturer. + fontRevision: sfnt.Fixed align(1), + + /// To compute: set it to 0, sum the entire font as uint32, then store + /// 0xB1B0AFBA - sum. If the font is used as a component in a font + /// collection file, the value of this field will be invalidated by + /// changes to the file structure and font table directory, and must + /// be ignored. + checksumAdjustment: sfnt.uint32 align(1), + + /// Set to 0x5F0F3CF5. + magicNumber: sfnt.uint32 align(1), + + /// Bit 0: Baseline for font at y=0. + /// + /// Bit 1: Left sidebearing point at x=0 + /// (relevant only for TrueType rasterizers) + /// + /// Bit 2: Instructions may depend on point size. + /// + /// Bit 3: Force ppem to integer values for all internal scaler math; may + /// use fractional ppem sizes if this bit is clear. It is strongly + /// recommended that this be set in hinted fonts. + /// + /// Bit 4: Instructions may alter advance width + /// (the advance widths might not scale linearly). + /// + /// Bit 5: This bit is not used in OpenType, and should not be set in order + /// to ensure compatible behavior on all platforms. If set, it may + /// result in different behavior for vertical layout in some + /// platforms. + /// + /// (See Apple’s specification for details + /// regarding behavior in Apple platforms.) + /// + /// Bits 6 – 10: These bits are not used in OpenType and should always be + /// cleared. + /// + /// (See Apple’s specification for details + /// regarding legacy use in Apple platforms.) + /// + /// Bit 11: Font data is “lossless” as a result of having been + /// subjected to optimizing transformation and/or compression + /// (such as compression mechanisms defined by ISO/IEC 14496-18, + /// MicroType® Express, WOFF 2.0, or similar) where the original + /// font functionality and features are retained but the binary + /// compatibility between input and output font files is not + /// guaranteed. As a result of the applied transform, the DSIG + /// table may also be invalidated. + /// + /// Bit 12: Font converted (produce compatible metrics). + /// + /// Bit 13: Font optimized for ClearType®. Note, fonts that rely on embedded + /// bitmaps (EBDT) for rendering should not be considered optimized + /// for ClearType, and therefore should keep this bit cleared. + /// + /// Bit 14: Last Resort font. If set, indicates that the glyphs encoded in + /// the 'cmap' subtables are simply generic symbolic representations + /// of code point ranges and do not truly represent support for + /// those code points. If unset, indicates that the glyphs encoded + /// in the 'cmap' subtables represent proper support for those code + /// points. + /// + /// Bit 15: Reserved, set to 0. + flags: sfnt.uint16 align(1), + + /// Set to a value from 16 to 16384. Any value in this range is valid. + /// + /// In fonts that have TrueType outlines, a power of 2 is recommended + /// as this allows performance optimization in some rasterizers. + unitsPerEm: sfnt.uint16 align(1), + + /// Number of seconds since 12:00 midnight that started + /// January 1st, 1904, in GMT/UTC time zone. + created: sfnt.LONGDATETIME align(1), + + /// Number of seconds since 12:00 midnight that started + /// January 1st, 1904, in GMT/UTC time zone. + modified: sfnt.LONGDATETIME align(1), + + /// Minimum x coordinate across all glyph bounding boxes. + xMin: sfnt.int16 align(1), + + /// Minimum y coordinate across all glyph bounding boxes. + yMin: sfnt.int16 align(1), + + /// Maximum x coordinate across all glyph bounding boxes. + xMax: sfnt.int16 align(1), + + /// Maximum y coordinate across all glyph bounding boxes. + yMax: sfnt.int16 align(1), + + /// Bit 0: Bold (if set to 1); + /// Bit 1: Italic (if set to 1) + /// Bit 2: Underline (if set to 1) + /// Bit 3: Outline (if set to 1) + /// Bit 4: Shadow (if set to 1) + /// Bit 5: Condensed (if set to 1) + /// Bit 6: Extended (if set to 1) + /// Bits 7 – 15: Reserved (set to 0). + macStyle: sfnt.uint16 align(1), + + /// Smallest readable size in pixels. + lowestRecPPEM: sfnt.uint16 align(1), + + /// Deprecated (Set to 2). + /// 0: Fully mixed directional glyphs; + /// 1: Only strongly left to right; + /// 2: Like 1 but also contains neutrals; + /// -1: Only strongly right to left; + /// -2: Like -1 but also contains neutrals. + fontDirectionHint: sfnt.int16 align(1), + + /// 0 for short offsets (Offset16), 1 for long (Offset32). + indexToLocFormat: sfnt.int16 align(1), + + /// 0 for current format. + glyphDataFormat: sfnt.int16 align(1), + + /// Parse the table from raw data. + pub fn init(data: []const u8) !Head { + var fbs = std.io.fixedBufferStream(data); + const reader = fbs.reader(); + + return try reader.readStructEndian(Head, .big); + } +}; + +test "head" { + const testing = std.testing; + const alloc = testing.allocator; + const test_font = @import("../embedded.zig").julia_mono; + + const font = try sfnt.SFNT.init(test_font, alloc); + defer font.deinit(alloc); + + const table = font.getTable("head").?; + + const head = try Head.init(table); + + try testing.expectEqualDeep( + Head{ + .majorVersion = 1, + .minorVersion = 0, + .fontRevision = sfnt.Fixed.from(0.05499267578125), + .checksumAdjustment = 1007668681, + .magicNumber = 1594834165, + .flags = 7, + .unitsPerEm = 2000, + .created = 3797757830, + .modified = 3797760444, + .xMin = -1000, + .yMin = -1058, + .xMax = 3089, + .yMax = 2400, + .macStyle = 0, + .lowestRecPPEM = 7, + .fontDirectionHint = 2, + .indexToLocFormat = 1, + .glyphDataFormat = 0, + }, + head, + ); +} diff --git a/src/font/opentype/hhea.zig b/src/font/opentype/hhea.zig new file mode 100644 index 000000000..8f224e1bb --- /dev/null +++ b/src/font/opentype/hhea.zig @@ -0,0 +1,115 @@ +const std = @import("std"); +const assert = std.debug.assert; +const sfnt = @import("sfnt.zig"); + +/// Horizontal Header Table +/// +/// References: +/// - https://learn.microsoft.com/en-us/typography/opentype/spec/hhea +pub const Hhea = extern struct { + /// Major version number of the horizontal header table — set to 1. + majorVersion: sfnt.uint16 align(1), + + /// Minor version number of the horizontal header table — set to 0. + minorVersion: sfnt.uint16 align(1), + + /// Typographic ascent—see remarks below. + ascender: sfnt.FWORD align(1), + + /// Typographic descent—see remarks below. + descender: sfnt.FWORD align(1), + + /// Typographic line gap. + /// + /// Negative lineGap values are treated as zero + /// in some legacy platform implementations. + lineGap: sfnt.FWORD align(1), + + /// Maximum advance width value in 'hmtx' table. + advanceWidthMax: sfnt.UFWORD align(1), + + /// Minimum left sidebearing value in 'hmtx' table for + /// glyphs with contours (empty glyphs should be ignored). + minLeftSideBearing: sfnt.FWORD align(1), + + /// Minimum right sidebearing value; calculated as + /// min(aw - (lsb + xMax - xMin)) for glyphs with + /// contours (empty glyphs should be ignored). + minRightSideBearing: sfnt.FWORD align(1), + + /// Max(lsb + (xMax - xMin)). + xMaxExtent: sfnt.FWORD align(1), + + /// Used to calculate the slope of the cursor (rise/run); 1 for vertical. + caretSlopeRise: sfnt.int16 align(1), + + /// 0 for vertical. + caretSlopeRun: sfnt.int16 align(1), + + /// The amount by which a slanted highlight on a glyph needs to be shifted + /// to produce the best appearance. Set to 0 for non-slanted fonts + caretOffset: sfnt.int16 align(1), + + /// set to 0 + _reserved0: sfnt.int16 align(1), + + /// set to 0 + _reserved1: sfnt.int16 align(1), + + /// set to 0 + _reserved2: sfnt.int16 align(1), + + /// set to 0 + _reserved3: sfnt.int16 align(1), + + /// 0 for current format. + metricDataFormat: sfnt.int16 align(1), + + /// Number of hMetric entries in 'hmtx' table + numberOfHMetrics: sfnt.uint16 align(1), + + /// Parse the table from raw data. + pub fn init(data: []const u8) !Hhea { + var fbs = std.io.fixedBufferStream(data); + const reader = fbs.reader(); + + return try reader.readStructEndian(Hhea, .big); + } +}; + +test "hhea" { + const testing = std.testing; + const alloc = testing.allocator; + const test_font = @import("../embedded.zig").julia_mono; + + const font = try sfnt.SFNT.init(test_font, alloc); + defer font.deinit(alloc); + + const table = font.getTable("hhea").?; + + const hhea = try Hhea.init(table); + + try testing.expectEqualDeep( + Hhea{ + .majorVersion = 1, + .minorVersion = 0, + .ascender = 1900, + .descender = -450, + .lineGap = 0, + .advanceWidthMax = 1200, + .minLeftSideBearing = -1000, + .minRightSideBearing = -1889, + .xMaxExtent = 3089, + .caretSlopeRise = 1, + .caretSlopeRun = 0, + .caretOffset = 0, + ._reserved0 = 0, + ._reserved1 = 0, + ._reserved2 = 0, + ._reserved3 = 0, + .metricDataFormat = 0, + .numberOfHMetrics = 2, + }, + hhea, + ); +} diff --git a/src/font/opentype/os2.zig b/src/font/opentype/os2.zig new file mode 100644 index 000000000..9bd57fc37 --- /dev/null +++ b/src/font/opentype/os2.zig @@ -0,0 +1,577 @@ +const std = @import("std"); +const assert = std.debug.assert; +const sfnt = @import("sfnt.zig"); + +pub const FSSelection = packed struct(sfnt.uint16) { + /// Font contains italic or oblique glyphs, otherwise they are upright. + italic: bool = false, + + /// Glyphs are underscored. + underscore: bool = false, + + /// Glyphs have their foreground and background reversed. + negative: bool = false, + + /// Outline (hollow) glyphs, otherwise they are solid. + outlined: bool = false, + + /// Glyphs are overstruck. + strikeout: bool = false, + + /// Glyphs are emboldened. + bold: bool = false, + + /// Glyphs are in the standard weight/style for the font. + regular: bool = false, + + /// If set, it is strongly recommended that applications use + /// OS/2.sTypoAscender - OS/2.sTypoDescender + OS/2.sTypoLineGap + /// as the default line spacing for this font. + use_typo_metrics: bool = false, + + /// The font has 'name' table strings consistent with a weight/width/slope + /// family without requiring use of name IDs 21 and 22. + wws: bool = false, + + /// Font contains oblique glyphs. + oblique: bool = false, + + _reserved: u6 = 0, +}; + +/// OS/2 and Windows Metrics Table +/// +/// References: +/// - https://learn.microsoft.com/en-us/typography/opentype/spec/os2 +pub const OS2v5 = extern struct { + version: sfnt.uint16 align(1), + xAvgCharWidth: sfnt.FWORD align(1), + usWeightClass: sfnt.uint16 align(1), + usWidthClass: sfnt.uint16 align(1), + fsType: sfnt.uint16 align(1), + ySubscriptXSize: sfnt.FWORD align(1), + ySubscriptYSize: sfnt.FWORD align(1), + ySubscriptXOffset: sfnt.FWORD align(1), + ySubscriptYOffset: sfnt.FWORD align(1), + ySuperscriptXSize: sfnt.FWORD align(1), + ySuperscriptYSize: sfnt.FWORD align(1), + ySuperscriptXOffset: sfnt.FWORD align(1), + ySuperscriptYOffset: sfnt.FWORD align(1), + yStrikeoutSize: sfnt.FWORD align(1), + yStrikeoutPosition: sfnt.FWORD align(1), + sFamilyClass: sfnt.int16 align(1), + panose: [10]sfnt.uint8 align(1), + ulUnicodeRange1: sfnt.uint32 align(1), + ulUnicodeRange2: sfnt.uint32 align(1), + ulUnicodeRange3: sfnt.uint32 align(1), + ulUnicodeRange4: sfnt.uint32 align(1), + achVendID: sfnt.Tag align(1), + fsSelection: FSSelection align(1), + usFirstCharIndex: sfnt.uint16 align(1), + usLastCharIndex: sfnt.uint16 align(1), + sTypoAscender: sfnt.FWORD align(1), + sTypoDescender: sfnt.FWORD align(1), + sTypoLineGap: sfnt.FWORD align(1), + usWinAscent: sfnt.UFWORD align(1), + usWinDescent: sfnt.UFWORD align(1), + ulCodePageRange1: sfnt.uint32 align(1), + ulCodePageRange2: sfnt.uint32 align(1), + sxHeight: sfnt.FWORD align(1), + sCapHeight: sfnt.FWORD align(1), + usDefaultChar: sfnt.uint16 align(1), + usBreakChar: sfnt.uint16 align(1), + usMaxContext: sfnt.uint16 align(1), + usLowerOpticalPointSize: sfnt.uint16 align(1), + usUpperOpticalPointSize: sfnt.uint16 align(1), +}; + +pub const OS2v4_3_2 = extern struct { + version: sfnt.uint16 align(1), + xAvgCharWidth: sfnt.FWORD align(1), + usWeightClass: sfnt.uint16 align(1), + usWidthClass: sfnt.uint16 align(1), + fsType: sfnt.uint16 align(1), + ySubscriptXSize: sfnt.FWORD align(1), + ySubscriptYSize: sfnt.FWORD align(1), + ySubscriptXOffset: sfnt.FWORD align(1), + ySubscriptYOffset: sfnt.FWORD align(1), + ySuperscriptXSize: sfnt.FWORD align(1), + ySuperscriptYSize: sfnt.FWORD align(1), + ySuperscriptXOffset: sfnt.FWORD align(1), + ySuperscriptYOffset: sfnt.FWORD align(1), + yStrikeoutSize: sfnt.FWORD align(1), + yStrikeoutPosition: sfnt.FWORD align(1), + sFamilyClass: sfnt.int16 align(1), + panose: [10]sfnt.uint8 align(1), + ulUnicodeRange1: sfnt.uint32 align(1), + ulUnicodeRange2: sfnt.uint32 align(1), + ulUnicodeRange3: sfnt.uint32 align(1), + ulUnicodeRange4: sfnt.uint32 align(1), + achVendID: sfnt.Tag align(1), + fsSelection: FSSelection align(1), + usFirstCharIndex: sfnt.uint16 align(1), + usLastCharIndex: sfnt.uint16 align(1), + sTypoAscender: sfnt.FWORD align(1), + sTypoDescender: sfnt.FWORD align(1), + sTypoLineGap: sfnt.FWORD align(1), + usWinAscent: sfnt.UFWORD align(1), + usWinDescent: sfnt.UFWORD align(1), + ulCodePageRange1: sfnt.uint32 align(1), + ulCodePageRange2: sfnt.uint32 align(1), + sxHeight: sfnt.FWORD align(1), + sCapHeight: sfnt.FWORD align(1), + usDefaultChar: sfnt.uint16 align(1), + usBreakChar: sfnt.uint16 align(1), + usMaxContext: sfnt.uint16 align(1), +}; + +pub const OS2v1 = extern struct { + version: sfnt.uint16 align(1), + xAvgCharWidth: sfnt.FWORD align(1), + usWeightClass: sfnt.uint16 align(1), + usWidthClass: sfnt.uint16 align(1), + fsType: sfnt.uint16 align(1), + ySubscriptXSize: sfnt.FWORD align(1), + ySubscriptYSize: sfnt.FWORD align(1), + ySubscriptXOffset: sfnt.FWORD align(1), + ySubscriptYOffset: sfnt.FWORD align(1), + ySuperscriptXSize: sfnt.FWORD align(1), + ySuperscriptYSize: sfnt.FWORD align(1), + ySuperscriptXOffset: sfnt.FWORD align(1), + ySuperscriptYOffset: sfnt.FWORD align(1), + yStrikeoutSize: sfnt.FWORD align(1), + yStrikeoutPosition: sfnt.FWORD align(1), + sFamilyClass: sfnt.int16 align(1), + panose: [10]sfnt.uint8 align(1), + ulUnicodeRange1: sfnt.uint32 align(1), + ulUnicodeRange2: sfnt.uint32 align(1), + ulUnicodeRange3: sfnt.uint32 align(1), + ulUnicodeRange4: sfnt.uint32 align(1), + achVendID: sfnt.Tag align(1), + fsSelection: FSSelection align(1), + usFirstCharIndex: sfnt.uint16 align(1), + usLastCharIndex: sfnt.uint16 align(1), + sTypoAscender: sfnt.FWORD align(1), + sTypoDescender: sfnt.FWORD align(1), + sTypoLineGap: sfnt.FWORD align(1), + usWinAscent: sfnt.UFWORD align(1), + usWinDescent: sfnt.UFWORD align(1), + ulCodePageRange1: sfnt.uint32 align(1), + ulCodePageRange2: sfnt.uint32 align(1), +}; + +pub const OS2v0 = extern struct { + version: sfnt.uint16 align(1), + xAvgCharWidth: sfnt.FWORD align(1), + usWeightClass: sfnt.uint16 align(1), + usWidthClass: sfnt.uint16 align(1), + fsType: sfnt.uint16 align(1), + ySubscriptXSize: sfnt.FWORD align(1), + ySubscriptYSize: sfnt.FWORD align(1), + ySubscriptXOffset: sfnt.FWORD align(1), + ySubscriptYOffset: sfnt.FWORD align(1), + ySuperscriptXSize: sfnt.FWORD align(1), + ySuperscriptYSize: sfnt.FWORD align(1), + ySuperscriptXOffset: sfnt.FWORD align(1), + ySuperscriptYOffset: sfnt.FWORD align(1), + yStrikeoutSize: sfnt.FWORD align(1), + yStrikeoutPosition: sfnt.FWORD align(1), + sFamilyClass: sfnt.int16 align(1), + panose: [10]sfnt.uint8 align(1), + ulUnicodeRange1: sfnt.uint32 align(1), + ulUnicodeRange2: sfnt.uint32 align(1), + ulUnicodeRange3: sfnt.uint32 align(1), + ulUnicodeRange4: sfnt.uint32 align(1), + achVendID: sfnt.Tag align(1), + fsSelection: FSSelection align(1), + usFirstCharIndex: sfnt.uint16 align(1), + usLastCharIndex: sfnt.uint16 align(1), + sTypoAscender: sfnt.FWORD align(1), + sTypoDescender: sfnt.FWORD align(1), + sTypoLineGap: sfnt.FWORD align(1), + usWinAscent: sfnt.UFWORD align(1), + usWinDescent: sfnt.UFWORD align(1), +}; + +/// Generic OS/2 table with optional fields +/// for those that don't exist in all versions. +/// +/// References: +/// - https://learn.microsoft.com/en-us/typography/opentype/spec/os2 +pub const OS2 = struct { + /// The version number for the OS/2 table: 0x0000 to 0x0005. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#version + version: u16, + /// The Average Character Width field specifies the arithmetic average of the escapement (width) of all non-zero width glyphs in the font. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#xavgcharwidth + xAvgCharWidth: i16, + /// Indicates the visual weight (degree of blackness or thickness of strokes) of the characters in the font. Values from 1 to 1000 are valid. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usweightclass + usWeightClass: u16, + /// Indicates a relative change from the normal aspect ratio (width to height ratio) as specified by a font designer for the glyphs in a font. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#uswidthclass + usWidthClass: u16, + /// Indicates font embedding licensing rights for the font. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#fstype + fsType: u16, + /// The recommended horizontal size in font design units for subscripts for this font. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysubscriptxsize + ySubscriptXSize: i16, + /// The recommended vertical size in font design units for subscripts for this font. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysubscriptysize + ySubscriptYSize: i16, + /// The recommended horizontal offset in font design units for subscripts for this font. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysubscriptxoffset + ySubscriptXOffset: i16, + /// The recommended vertical offset in font design units from the baseline for subscripts for this font. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysubscriptyoffset + ySubscriptYOffset: i16, + /// The recommended horizontal size in font design units for superscripts for this font. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysuperscriptxsize + ySuperscriptXSize: i16, + /// The recommended vertical size in font design units for superscripts for this font. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysuperscriptysize + ySuperscriptYSize: i16, + /// The recommended horizontal offset in font design units for superscripts for this font. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysuperscriptxoffset + ySuperscriptXOffset: i16, + /// The recommended vertical offset in font design units from the baseline for superscripts for this font. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ysuperscriptyoffset + ySuperscriptYOffset: i16, + /// Thickness of the strikeout stroke in font design units. Should be > 0. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ystrikeoutsize + yStrikeoutSize: i16, + /// The position of the top of the strikeout stroke relative to the baseline in font design units. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ystrikeoutposition + yStrikeoutPosition: i16, + /// This field provides a classification of font-family design. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#sfamilyclass + sFamilyClass: i16, + /// This 10-byte array of numbers is used to describe the visual characteristics of a given typeface. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#panose + panose: [10]u8, + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulunicoderange + ulUnicodeRange1: u32, + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulunicoderange + ulUnicodeRange2: u32, + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulunicoderange + ulUnicodeRange3: u32, + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulunicoderange + ulUnicodeRange4: u32, + /// The four character identifier for the vendor of the given type face. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#achvendid + achVendID: [4]u8, + /// Contains information concerning the nature of the font patterns. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#fsselection + fsSelection: FSSelection, + /// The minimum Unicode index (character code) in this font, according to the 'cmap' subtable for platform ID 3 and platform-specific encoding ID 0 or 1. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usfirstcharindex + usFirstCharIndex: u16, + /// The maximum Unicode index (character code) in this font, according to the 'cmap' subtable for platform ID 3 and encoding ID 0 or 1. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#uslastcharindex + usLastCharIndex: u16, + /// The typographic ascender for this font. This field should be combined with the sTypoDescender and sTypoLineGap values to determine default line spacing. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#stypoascender + sTypoAscender: i16, + /// The typographic descender for this font. This field should be combined with the sTypoAscender and sTypoLineGap values to determine default line spacing. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#stypodescender + sTypoDescender: i16, + /// The typographic line gap for this font. This field should be combined with the sTypoAscender and sTypoDescender values to determine default line spacing. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#stypolinegap + sTypoLineGap: i16, + /// The “Windows ascender” metric. This should be used to specify the height above the baseline for a clipping region. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#uswinascent + usWinAscent: u16, + /// The “Windows descender” metric. This should be used to specify the vertical extent below the baseline for a clipping region. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#uswindescent + usWinDescent: u16, + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulcodepagerange + ulCodePageRange1: ?u32 = null, + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#ulcodepagerange + ulCodePageRange2: ?u32 = null, + /// This metric specifies the distance between the baseline and the approximate height of non-ascending lowercase letters measured in font design units. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#sxheight + sxHeight: ?i16 = null, + /// This metric specifies the distance between the baseline and the approximate height of uppercase letters measured in font design units. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#scapheight + sCapHeight: ?i16 = null, + /// This is the Unicode code point, in UTF-16 encoding, of a character that can be used for a default glyph if a requested character is not supported in the font. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usdefaultchar + usDefaultChar: ?u16 = null, + /// This is the Unicode code point, in UTF-16 encoding, of a character that can be used as a default break character. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usbreakchar + usBreakChar: ?u16 = null, + /// The maximum length of a target glyph context for any feature in this font. For example, a font which has only a pair kerning feature should set this field to 2. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usmaxcontext + usMaxContext: ?u16 = null, + /// This field is used for fonts with multiple optical styles. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usloweropticalpointsize + usLowerOpticalPointSize: ?u16 = null, + /// This field is used for fonts with multiple optical styles. + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2/#usupperopticalpointsize + usUpperOpticalPointSize: ?u16 = null, + + /// Parse the table from raw data. + pub fn init(data: []const u8) !OS2 { + var fbs = std.io.fixedBufferStream(data); + const reader = fbs.reader(); + + const version = try reader.readInt(sfnt.uint16, .big); + + // Return to the start, cause the version is part of the struct. + try fbs.seekTo(0); + + switch (version) { + 5 => { + const table = try reader.readStructEndian(OS2v5, .big); + return .{ + .version = table.version, + .xAvgCharWidth = table.xAvgCharWidth, + .usWeightClass = table.usWeightClass, + .usWidthClass = table.usWidthClass, + .fsType = table.fsType, + .ySubscriptXSize = table.ySubscriptXSize, + .ySubscriptYSize = table.ySubscriptYSize, + .ySubscriptXOffset = table.ySubscriptXOffset, + .ySubscriptYOffset = table.ySubscriptYOffset, + .ySuperscriptXSize = table.ySuperscriptXSize, + .ySuperscriptYSize = table.ySuperscriptYSize, + .ySuperscriptXOffset = table.ySuperscriptXOffset, + .ySuperscriptYOffset = table.ySuperscriptYOffset, + .yStrikeoutSize = table.yStrikeoutSize, + .yStrikeoutPosition = table.yStrikeoutPosition, + .sFamilyClass = table.sFamilyClass, + .panose = table.panose, + .ulUnicodeRange1 = table.ulUnicodeRange1, + .ulUnicodeRange2 = table.ulUnicodeRange2, + .ulUnicodeRange3 = table.ulUnicodeRange3, + .ulUnicodeRange4 = table.ulUnicodeRange4, + .achVendID = table.achVendID, + .fsSelection = table.fsSelection, + .usFirstCharIndex = table.usFirstCharIndex, + .usLastCharIndex = table.usLastCharIndex, + .sTypoAscender = table.sTypoAscender, + .sTypoDescender = table.sTypoDescender, + .sTypoLineGap = table.sTypoLineGap, + .usWinAscent = table.usWinAscent, + .usWinDescent = table.usWinDescent, + .ulCodePageRange1 = table.ulCodePageRange1, + .ulCodePageRange2 = table.ulCodePageRange2, + .sxHeight = table.sxHeight, + .sCapHeight = table.sCapHeight, + .usDefaultChar = table.usDefaultChar, + .usBreakChar = table.usBreakChar, + .usMaxContext = table.usMaxContext, + .usLowerOpticalPointSize = table.usLowerOpticalPointSize, + .usUpperOpticalPointSize = table.usUpperOpticalPointSize, + }; + }, + 4, 3, 2 => { + const table = try reader.readStructEndian(OS2v4_3_2, .big); + return .{ + .version = table.version, + .xAvgCharWidth = table.xAvgCharWidth, + .usWeightClass = table.usWeightClass, + .usWidthClass = table.usWidthClass, + .fsType = table.fsType, + .ySubscriptXSize = table.ySubscriptXSize, + .ySubscriptYSize = table.ySubscriptYSize, + .ySubscriptXOffset = table.ySubscriptXOffset, + .ySubscriptYOffset = table.ySubscriptYOffset, + .ySuperscriptXSize = table.ySuperscriptXSize, + .ySuperscriptYSize = table.ySuperscriptYSize, + .ySuperscriptXOffset = table.ySuperscriptXOffset, + .ySuperscriptYOffset = table.ySuperscriptYOffset, + .yStrikeoutSize = table.yStrikeoutSize, + .yStrikeoutPosition = table.yStrikeoutPosition, + .sFamilyClass = table.sFamilyClass, + .panose = table.panose, + .ulUnicodeRange1 = table.ulUnicodeRange1, + .ulUnicodeRange2 = table.ulUnicodeRange2, + .ulUnicodeRange3 = table.ulUnicodeRange3, + .ulUnicodeRange4 = table.ulUnicodeRange4, + .achVendID = table.achVendID, + .fsSelection = table.fsSelection, + .usFirstCharIndex = table.usFirstCharIndex, + .usLastCharIndex = table.usLastCharIndex, + .sTypoAscender = table.sTypoAscender, + .sTypoDescender = table.sTypoDescender, + .sTypoLineGap = table.sTypoLineGap, + .usWinAscent = table.usWinAscent, + .usWinDescent = table.usWinDescent, + .ulCodePageRange1 = table.ulCodePageRange1, + .ulCodePageRange2 = table.ulCodePageRange2, + .sxHeight = table.sxHeight, + .sCapHeight = table.sCapHeight, + .usDefaultChar = table.usDefaultChar, + .usBreakChar = table.usBreakChar, + .usMaxContext = table.usMaxContext, + }; + }, + 1 => { + const table = try reader.readStructEndian(OS2v1, .big); + return .{ + .version = table.version, + .xAvgCharWidth = table.xAvgCharWidth, + .usWeightClass = table.usWeightClass, + .usWidthClass = table.usWidthClass, + .fsType = table.fsType, + .ySubscriptXSize = table.ySubscriptXSize, + .ySubscriptYSize = table.ySubscriptYSize, + .ySubscriptXOffset = table.ySubscriptXOffset, + .ySubscriptYOffset = table.ySubscriptYOffset, + .ySuperscriptXSize = table.ySuperscriptXSize, + .ySuperscriptYSize = table.ySuperscriptYSize, + .ySuperscriptXOffset = table.ySuperscriptXOffset, + .ySuperscriptYOffset = table.ySuperscriptYOffset, + .yStrikeoutSize = table.yStrikeoutSize, + .yStrikeoutPosition = table.yStrikeoutPosition, + .sFamilyClass = table.sFamilyClass, + .panose = table.panose, + .ulUnicodeRange1 = table.ulUnicodeRange1, + .ulUnicodeRange2 = table.ulUnicodeRange2, + .ulUnicodeRange3 = table.ulUnicodeRange3, + .ulUnicodeRange4 = table.ulUnicodeRange4, + .achVendID = table.achVendID, + .fsSelection = table.fsSelection, + .usFirstCharIndex = table.usFirstCharIndex, + .usLastCharIndex = table.usLastCharIndex, + .sTypoAscender = table.sTypoAscender, + .sTypoDescender = table.sTypoDescender, + .sTypoLineGap = table.sTypoLineGap, + .usWinAscent = table.usWinAscent, + .usWinDescent = table.usWinDescent, + .ulCodePageRange1 = table.ulCodePageRange1, + .ulCodePageRange2 = table.ulCodePageRange2, + }; + }, + 0 => { + const table = try reader.readStructEndian(OS2v0, .big); + return .{ + .version = table.version, + .xAvgCharWidth = table.xAvgCharWidth, + .usWeightClass = table.usWeightClass, + .usWidthClass = table.usWidthClass, + .fsType = table.fsType, + .ySubscriptXSize = table.ySubscriptXSize, + .ySubscriptYSize = table.ySubscriptYSize, + .ySubscriptXOffset = table.ySubscriptXOffset, + .ySubscriptYOffset = table.ySubscriptYOffset, + .ySuperscriptXSize = table.ySuperscriptXSize, + .ySuperscriptYSize = table.ySuperscriptYSize, + .ySuperscriptXOffset = table.ySuperscriptXOffset, + .ySuperscriptYOffset = table.ySuperscriptYOffset, + .yStrikeoutSize = table.yStrikeoutSize, + .yStrikeoutPosition = table.yStrikeoutPosition, + .sFamilyClass = table.sFamilyClass, + .panose = table.panose, + .ulUnicodeRange1 = table.ulUnicodeRange1, + .ulUnicodeRange2 = table.ulUnicodeRange2, + .ulUnicodeRange3 = table.ulUnicodeRange3, + .ulUnicodeRange4 = table.ulUnicodeRange4, + .achVendID = table.achVendID, + .fsSelection = table.fsSelection, + .usFirstCharIndex = table.usFirstCharIndex, + .usLastCharIndex = table.usLastCharIndex, + .sTypoAscender = table.sTypoAscender, + .sTypoDescender = table.sTypoDescender, + .sTypoLineGap = table.sTypoLineGap, + .usWinAscent = table.usWinAscent, + .usWinDescent = table.usWinDescent, + }; + }, + else => return error.OS2VersionNotSupported, + } + } +}; + +test "OS/2" { + const testing = std.testing; + const alloc = testing.allocator; + const test_font = @import("../embedded.zig").julia_mono; + + const font = try sfnt.SFNT.init(test_font, alloc); + defer font.deinit(alloc); + + const table = font.getTable("OS/2").?; + + const os2 = try OS2.init(table); + + try testing.expectEqualDeep(OS2{ + .version = 4, + .xAvgCharWidth = 1200, + .usWeightClass = 400, + .usWidthClass = 5, + .fsType = 0, + .ySubscriptXSize = 1300, + .ySubscriptYSize = 1200, + .ySubscriptXOffset = 0, + .ySubscriptYOffset = 150, + .ySuperscriptXSize = 1300, + .ySuperscriptYSize = 1200, + .ySuperscriptXOffset = 0, + .ySuperscriptYOffset = 700, + .yStrikeoutSize = 100, + .yStrikeoutPosition = 550, + .sFamilyClass = 0, + .panose = .{ 2, 11, 6, 9, 6, 3, 0, 2, 0, 4 }, + .ulUnicodeRange1 = 3843162111, + .ulUnicodeRange2 = 3603300351, + .ulUnicodeRange3 = 117760229, + .ulUnicodeRange4 = 96510060, + .achVendID = "corm".*, + .fsSelection = .{ + .regular = true, + .use_typo_metrics = true, + }, + .usFirstCharIndex = 13, + .usLastCharIndex = 65535, + .sTypoAscender = 1900, + .sTypoDescender = -450, + .sTypoLineGap = 0, + .usWinAscent = 2400, + .usWinDescent = 450, + .ulCodePageRange1 = 1613234687, + .ulCodePageRange2 = 0, + .sxHeight = 1100, + .sCapHeight = 1450, + .usDefaultChar = 0, + .usBreakChar = 32, + .usMaxContext = 126, + .usLowerOpticalPointSize = null, + .usUpperOpticalPointSize = null, + }, os2); +} diff --git a/src/font/opentype/post.zig b/src/font/opentype/post.zig new file mode 100644 index 000000000..e2c98bca1 --- /dev/null +++ b/src/font/opentype/post.zig @@ -0,0 +1,82 @@ +const std = @import("std"); +const assert = std.debug.assert; +const sfnt = @import("sfnt.zig"); + +/// PostScript Table +/// +/// This implementation doesn't parse the +/// extra fields in versions 2.0 and 2.5. +/// +/// References: +/// - https://learn.microsoft.com/en-us/typography/opentype/spec/post +pub const Post = extern struct { + version: sfnt.Version16Dot16 align(1), + + /// Italic angle in counter-clockwise degrees from the vertical. + /// Zero for upright text, negative for text that leans to the + /// right (forward). + italicAngle: sfnt.Fixed align(1), + + /// Suggested y-coordinate of the top of the underline. + underlinePosition: sfnt.FWORD align(1), + + /// Suggested values for the underline thickness. + /// In general, the underline thickness should match the thickness of + /// the underscore character (U+005F LOW LINE), and should also match + /// the strikeout thickness, which is specified in the OS/2 table. + underlineThickness: sfnt.FWORD align(1), + + /// Set to 0 if the font is proportionally spaced, non-zero if + /// the font is not proportionally spaced (i.e. monospaced). + isFixedPitch: sfnt.uint32 align(1), + + /// Minimum memory usage when an OpenType font is downloaded. + minMemType42: sfnt.uint32 align(1), + + /// Maximum memory usage when an OpenType font is downloaded. + maxMemType42: sfnt.uint32 align(1), + + /// Minimum memory usage when an OpenType + /// font is downloaded as a Type 1 font. + minMemType1: sfnt.uint32 align(1), + + /// Maximum memory usage when an OpenType + /// font is downloaded as a Type 1 font. + maxMemType1: sfnt.uint32 align(1), + + /// Parse the table from raw data. + pub fn init(data: []const u8) !Post { + var fbs = std.io.fixedBufferStream(data); + const reader = fbs.reader(); + + return try reader.readStructEndian(Post, .big); + } +}; + +test "post" { + const testing = std.testing; + const alloc = testing.allocator; + const test_font = @import("../embedded.zig").julia_mono; + + const font = try sfnt.SFNT.init(test_font, alloc); + defer font.deinit(alloc); + + const table = font.getTable("post").?; + + const post = try Post.init(table); + + try testing.expectEqualDeep( + Post{ + .version = sfnt.Version16Dot16{ .minor = 0, .major = 2 }, + .italicAngle = sfnt.Fixed.from(0.0), + .underlinePosition = -200, + .underlineThickness = 100, + .isFixedPitch = 1, + .minMemType42 = 0, + .maxMemType42 = 0, + .minMemType1 = 0, + .maxMemType1 = 0, + }, + post, + ); +} diff --git a/src/font/opentype/sfnt.zig b/src/font/opentype/sfnt.zig new file mode 100644 index 000000000..cbce50455 --- /dev/null +++ b/src/font/opentype/sfnt.zig @@ -0,0 +1,314 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +/// 8-bit unsigned integer. +pub const uint8 = u8; + +/// 8-bit signed integer. +pub const int8 = i8; + +/// 16-bit unsigned integer. +pub const uint16 = u16; + +/// 16-bit signed integer. +pub const int16 = i16; + +/// 24-bit unsigned integer. +pub const uint24 = u24; + +/// 32-bit unsigned integer. +pub const uint32 = u32; + +/// 32-bit signed integer. +pub const int32 = i32; + +/// 32-bit signed fixed-point number (16.16) +pub const Fixed = FixedPoint(i32, 16, 16); + +/// int16 that describes a quantity in font design units. +pub const FWORD = i16; + +/// uint16 that describes a quantity in font design units. +pub const UFWORD = u16; + +/// 16-bit signed fixed number with the low 14 bits of fraction (2.14). +pub const F2DOT14 = FixedPoint(i16, 2, 14); + +/// Date and time represented in number of seconds since 12:00 midnight, January 1, 1904, UTC. The value is represented as a signed 64-bit integer. +pub const LONGDATETIME = i64; + +/// Array of four uint8s (length = 32 bits) used to identify a table, +/// design-variation axis, script, language system, feature, or baseline. +pub const Tag = [4]u8; + +/// 8-bit offset to a table, same as uint8, NULL offset = 0x00 +pub const Offset8 = u8; + +/// Short offset to a table, same as uint16, NULL offset = 0x0000 +pub const Offset16 = u16; + +/// 24-bit offset to a table, same as uint24, NULL offset = 0x000000 +pub const Offset24 = u24; + +/// Long offset to a table, same as uint32, NULL offset = 0x00000000 +pub const Offset32 = u32; + +/// Packed 32-bit value with major and minor version numbers +pub const Version16Dot16 = packed struct(u32) { + minor: u16, + major: u16, +}; + +/// 32-bit signed 26.6 fixed point numbers. +pub const F26Dot6 = FixedPoint(i32, 26, 6); + +fn FixedPoint(comptime T: type, int_bits: u64, frac_bits: u64) type { + const type_info: std.builtin.Type.Int = @typeInfo(T).Int; + comptime assert(int_bits + frac_bits == type_info.bits); + + return packed struct(T) { + const Self = FixedPoint(T, int_bits, frac_bits); + const frac_factor: comptime_float = @floatFromInt(std.math.pow( + u64, + 2, + frac_bits, + )); + const half = @as(T, 1) << @intCast(frac_bits - 1); + + frac: std.meta.Int(.unsigned, frac_bits), + int: std.meta.Int(type_info.signedness, int_bits), + + pub fn to(self: Self, comptime FloatType: type) FloatType { + const i: FloatType = @floatFromInt(self.int); + const f: FloatType = @floatFromInt(self.frac); + + return i + f / frac_factor; + } + + pub fn from(float: anytype) Self { + const int = @floor(float); + const frac = @abs(float - int); + + return .{ + .int = @intFromFloat(int), + .frac = @intFromFloat(@round(frac * frac_factor)), + }; + } + + /// Round to the nearest integer, .5 rounds away from 0. + pub fn round(self: Self) T { + if (self.frac & half != 0) + return self.int + 1 + else + return self.int; + } + + pub fn format( + self: Self, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, + ) !void { + _ = fmt; + _ = options; + + try writer.print("{d}", .{self.to(f64)}); + } + }; +} + +test FixedPoint { + const testing = std.testing; + + const p26d6 = F26Dot6.from(26.6); + try testing.expectEqual(F26Dot6{ + .int = 26, + .frac = 38, + }, p26d6); + try testing.expectEqual(26.59375, p26d6.to(f64)); + try testing.expectEqual(27, p26d6.round()); + + const n26d6 = F26Dot6.from(-26.6); + try testing.expectEqual(F26Dot6{ + .int = -27, + .frac = 26, + }, n26d6); + try testing.expectEqual(-26.59375, n26d6.to(f64)); + try testing.expectEqual(-27, n26d6.round()); +} + +/// Wrapper for parsing a SFNT font and accessing its tables. +/// +/// References: +/// - https://learn.microsoft.com/en-us/typography/opentype/spec/otff +/// - https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6.html +pub const SFNT = struct { + const Directory = struct { + offset: OffsetSubtable, + records: []TableRecord, + + /// The static (fixed-sized) portion of the table directory + /// + /// This struct matches the memory layout of the TrueType/OpenType + /// TableDirectory, but does not include the TableRecord array, since + /// that is dynamically sized, so we parse it separately. + /// + /// In the TrueType reference manual this + /// is referred to as the "offset subtable". + /// + /// https://learn.microsoft.com/en-us/typography/opentype/spec/otff#table-directory + const OffsetSubtable = extern struct { + /// Indicates the type of font file we're reading. + /// - 0x00_01_00_00 ---- TrueType + /// - 0x74_72_75_65 'true' TrueType + /// - 0x4F_54_54_4F 'OTTO' OpenType + /// - 0x74_79_70_31 'typ1' PostScript + sfnt_version: uint32 align(1), + /// Number of tables. + num_tables: uint16 align(1), + /// Maximum power of 2 less than or equal to numTables, times 16 ((2**floor(log2(numTables))) * 16, where “**” is an exponentiation operator). + search_range: uint16 align(1), + /// Log2 of the maximum power of 2 less than or equal to numTables (log2(searchRange/16), which is equal to floor(log2(numTables))). + entry_selector: uint16 align(1), + /// numTables times 16, minus searchRange ((numTables * 16) - searchRange). + range_shift: uint16 align(1), + + pub fn format( + self: OffsetSubtable, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, + ) !void { + _ = fmt; + _ = options; + + try writer.print( + "OffsetSubtable('{s}'){{ .num_tables = {} }}", + .{ + if (self.sfnt_version == 0x00_01_00_00) + &@as([10]u8, "0x00010000".*) + else + &@as([4]u8, @bitCast( + std.mem.nativeToBig(u32, self.sfnt_version), + )), + self.num_tables, + }, + ); + } + }; + + const TableRecord = extern struct { + /// Table identifier. + tag: Tag align(1), + /// Checksum for this table. + checksum: uint32 align(1), + /// Offset from beginning of font file. + offset: Offset32 align(1), + /// Length of this table. + length: uint32 align(1), + + pub fn format( + self: TableRecord, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, + ) !void { + _ = fmt; + _ = options; + + try writer.print( + "TableRecord(\"{s}\"){{ .checksum = {}, .offset = {}, .length = {} }}", + .{ + self.tag, + self.checksum, + self.offset, + self.length, + }, + ); + } + }; + }; + + directory: Directory, + + data: []const u8, + + /// Parse a font from raw data. The struct will keep a + /// reference to `data` and use it for future operations. + pub fn init(data: []const u8, alloc: Allocator) !SFNT { + var fbs = std.io.fixedBufferStream(data); + const reader = fbs.reader(); + + // SFNT files use big endian, if our native endian is + // not big we'll need to byte swap the values we read. + const byte_swap = native_endian != .big; + + var directory: Directory = undefined; + + try reader.readNoEof(std.mem.asBytes(&directory.offset)); + if (byte_swap) std.mem.byteSwapAllFields( + Directory.OffsetSubtable, + &directory.offset, + ); + + directory.records = try alloc.alloc(Directory.TableRecord, directory.offset.num_tables); + + try reader.readNoEof(std.mem.sliceAsBytes(directory.records)); + if (byte_swap) for (directory.records) |*record| { + std.mem.byteSwapAllFields( + Directory.TableRecord, + record, + ); + }; + + return .{ + .directory = directory, + .data = data, + }; + } + + pub fn deinit(self: SFNT, alloc: Allocator) void { + alloc.free(self.directory.records); + } + + /// Returns the bytes of the table with the provided tag if present. + pub fn getTable(self: SFNT, tag: *const [4]u8) ?[]const u8 { + for (self.directory.records) |record| { + if (std.mem.eql(u8, tag, &record.tag)) { + return self.data[record.offset..][0..record.length]; + } + } + + return null; + } +}; + +const native_endian = @import("builtin").target.cpu.arch.endian(); + +test "parse font" { + const testing = std.testing; + const alloc = testing.allocator; + + const test_font = @import("../embedded.zig").julia_mono; + + const sfnt = try SFNT.init(&test_font.*, alloc); + defer sfnt.deinit(alloc); + + try testing.expectEqual(19, sfnt.directory.offset.num_tables); + try testing.expectEqualStrings("prep", &sfnt.directory.records[18].tag); +} + +test "get table" { + const testing = std.testing; + const alloc = testing.allocator; + + const test_font = @import("../embedded.zig").julia_mono; + + const sfnt = try SFNT.init(&test_font.*, alloc); + defer sfnt.deinit(alloc); + + const svg = sfnt.getTable("SVG ").?; + + try testing.expectEqual(430, svg.len); +}