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)
This commit is contained in:
Qwerasd
2024-09-28 22:01:31 -06:00
parent 5dcca19038
commit 7e5a164be8
6 changed files with 1277 additions and 0 deletions

View File

@ -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());

179
src/font/opentype/head.zig Normal file
View File

@ -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 Apples specification for details
/// regarding behavior in Apple platforms.)
///
/// Bits 6 10: These bits are not used in OpenType and should always be
/// cleared.
///
/// (See Apples 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,
);
}

115
src/font/opentype/hhea.zig Normal file
View File

@ -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 ascentsee remarks below.
ascender: sfnt.FWORD align(1),
/// Typographic descentsee 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,
);
}

577
src/font/opentype/os2.zig Normal file
View File

@ -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);
}

View File

@ -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,
);
}

314
src/font/opentype/sfnt.zig Normal file
View File

@ -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);
}