file-parser/OpenFontFormat/OpenFont.py

6326 lines
197 KiB
Python
Raw Permalink Normal View History

2024-05-03 21:02:58 +10:00
from dataclasses import dataclass
2024-09-15 16:10:41 +10:00
from datetime import datetime
2024-05-03 21:02:58 +10:00
from enum import Enum, EnumMeta
from io import BytesIO
from math import floor, log2
2024-09-15 16:10:41 +10:00
from typing import Callable, Generic, List, Optional, Tuple, TypeVar, BinaryIO
2024-05-03 21:02:58 +10:00
2024-09-15 16:10:41 +10:00
from OFF_io_utils import SaveTell, null_if_zero, parse_at_offset, parse_at_offsets, parse_at_offsets_using_length, parse_at_optional_offset, parse_at_optional_offsets, parse_list_and_use_offsets_into, parse_list_at_offset, read_F2DOT14, read_fixed, read_fixed_version, read_long_datetime
from abcde import ABD, ABE, abstractmethod
2024-09-15 16:10:41 +10:00
from io_utils import Parser, is_at_end, len_to_end, read_ascii, read_i16, read_i32, read_i8, read_int, read_pascal_string, read_u16, read_u24, read_u32, read_u64, read_u8
2024-05-03 21:02:58 +10:00
2024-09-15 16:10:41 +10:00
T = TypeVar('T')
2024-05-03 21:02:58 +10:00
Tag_ = TypeVar('Tag_')
SomeTag = Callable[[str], Tag_] # If SomeTag is not an EnumMeta, it should throw a ValueError to indicate an invalid tag
2024-05-04 11:34:37 +10:00
def read_tag_with_conditions(f: BinaryIO, *conditions: Tuple[Callable[[str], bool], SomeTag[Tag_]], umbrellaTagCls: type | SomeTag[Tag_], strict:bool=True) -> Tag_:
2024-05-03 21:02:58 +10:00
tag = read_ascii(f, 4)
assert not strict or all([0x20 <= ord(c) <= 0x7e for c in tag]), f"Invalid tag: {[f.seek(-4, 1), f.read(4)][1]}"
for (condition, tagCls) in conditions:
if condition(tag):
try: return tagCls(tag)
except ValueError: pass
else:
assert False, f"Invalid {umbrellaTagCls.__name__}: '{tag}'"
2024-09-15 16:10:41 +10:00
always: Callable[[str], bool] = lambda _: True
2024-05-04 11:34:37 +10:00
def read_tag_from_tags(f: BinaryIO, *tagClss: SomeTag[Tag_], umbrellaTagCls: type | SomeTag[Tag_], strict:bool=True) -> Tag_:
2024-05-03 21:02:58 +10:00
"""
This is meant to be used for when some instances of an Enum are just CC01, CC02, CC03, ...
"""
2024-09-15 16:10:41 +10:00
return read_tag_with_conditions(f, *[(always, tagCls) for tagCls in tagClss], umbrellaTagCls=umbrellaTagCls, strict=strict)
2024-05-03 21:02:58 +10:00
2024-05-04 11:34:37 +10:00
def read_tag(f: BinaryIO, tagCls: SomeTag[Tag_], *, strict:bool=True) -> Tag_:
return read_tag_from_tags(f, tagCls, umbrellaTagCls=tagCls, strict=strict)
2024-05-03 21:02:58 +10:00
ID_ = TypeVar('ID_')
SomeID = Callable[[int], ID_]
2024-05-04 11:34:37 +10:00
def read_id_from_ranges(f: BinaryIO, *ranges: Tuple[Optional[int], SomeID[ID_]], umbrellaIdCls: SomeID[ID_], reader: Parser[int]=read_u16) -> ID_: # must be in ascending order
2024-05-03 21:02:58 +10:00
assert len(ranges) > 0, f"Must have at least one range"
id = reader(f)
for (num, idCls) in ranges:
if num is not None and id > num: continue
try: return idCls(id)
except ValueError: pass
2024-09-15 16:10:41 +10:00
assert False, f"Invalid {umbrellaIdCls.__name__}: {id} (hex: {repr_hex(id, 2)})"
2024-05-03 21:02:58 +10:00
2024-05-04 11:34:37 +10:00
def read_id(f: BinaryIO, idCls: SomeID[ID_], *, reader: Parser[int]=read_u16) -> ID_:
return read_id_from_ranges(f, (None, idCls), umbrellaIdCls=idCls, reader=reader)
2024-05-03 21:02:58 +10:00
@dataclass
class Table(ABD): pass
SomeTable = TypeVar('SomeTable', bound=Table)
@dataclass
class SetTable(Table, Generic[SomeTable]):
2024-05-04 11:34:37 +10:00
tableCount: int
tables: List[SomeTable]
2024-05-03 21:02:58 +10:00
def parse_set_table(f: BinaryIO, parser: Parser[SomeTable], *, offset_reader: Parser[int]=read_u16) -> SetTable[SomeTable]:
start_tell = f.tell()
count = read_u16(f)
offsets = [offset_reader(f) for _ in range(count)]
with SaveTell(f):
elements = parse_at_offsets(f, start_tell, offsets, parser)
return SetTable(count, elements)
@dataclass
class Record(ABD): pass
SomeRecord = TypeVar('SomeRecord', bound=Record)
@dataclass
class ListTable(Table, Generic[SomeRecord]):
numRecords: int
records: List[SomeRecord]
def parse_list_table(f: BinaryIO, parse_record: Parser[SomeRecord]) -> ListTable[SomeRecord]:
numRecords = read_u16(f)
records = [parse_record(f) for _ in range(numRecords)]
return ListTable(numRecords, records)
@dataclass
class OffsetTable(Table):
sfntVersion: int
numTables: int
searchRange: int
entrySelector: int
rangeShift: int
2024-09-15 16:10:41 +10:00
def repr_hex(value: int, length: int=8) -> str:
return f"0x{hex(value)[2:]:0>{length}}"
2024-05-03 21:02:58 +10:00
def parse_offset_table(f: BinaryIO) -> OffsetTable:
sfntVersion = read_u32(f)
2024-09-15 16:10:41 +10:00
assert sfntVersion in [0x00010000, 0x4F54544F], f"Invalid sfntVersion: {repr_hex(sfntVersion)}. Expected 0x00010000 or 0x4F54544F."
2024-05-03 21:02:58 +10:00
numTables = read_u16(f)
searchRange = read_u16(f)
entrySelector = read_u16(f)
rangeShift = read_u16(f)
assert bin(searchRange).count('1') == 1 # ensure searchRange is a power of 2
assert searchRange//16 <= numTables < searchRange//8 # ensure searchRange//16 is largest power of two less than num_tables
assert entrySelector == len(bin(searchRange))-7 # ensure entrySelector is the logarithm of searchRange//16
2024-09-15 16:10:41 +10:00
assert rangeShift == numTables*16-searchRange
2024-05-03 21:02:58 +10:00
return OffsetTable(sfntVersion, numTables, searchRange, entrySelector, rangeShift)
class TableTag(Enum):
# 5.2 (Mandatory)
Cmap = 'cmap'
Head = 'head'
Hhea = 'hhea'
Hmtx = 'hmtx'
Maxp = 'maxp'
Name = 'name'
OS2 = 'OS/2'
Post = 'post'
# 5.3 (TTF)
Cvt = 'cvt '
Fpgm = 'fpgm'
Glyf = 'glyf'
Loca = 'loca'
Prep = 'prep'
Gasp = 'gasp' # :O
# 5.4 (CFF)
...
# 5.5 (SVG)
Svg = 'SVG '
# 5.6 (Optional)
DSIG = 'DSIG'
Hdmx = 'hdmx'
2024-09-15 16:10:41 +10:00
Kern = 'kern'
2024-05-03 21:02:58 +10:00
LTSH = 'LTSH'
PCLT = 'PCLT'
VDMX = 'VDMX'
Vhea = 'vhea'
Vmtx = 'vmtx'
COLR = 'COLR'
CPAL = 'CPAL'
# 6.3 (Advanced Features)
BASE = 'BASE'
GDEF = 'GDEF'
GPOS = 'GPOS'
GSUB = 'GSUB'
JSTF = 'JSTF'
MATH = 'MATH'
# 7.3 (Variable Fonts)
Avar = 'avar'
Cvar = 'cvar'
Fvar = 'fvar'
Gvar = 'gvar'
HVAR = 'HVAR'
MVAR = 'MVAR'
STAT = 'STAT'
VVAR = 'VVAR'
# Tables as recognised by [FontTools](https://fonttools.readthedocs.io/en/latest/ttLib/tables.html)
Mort = 'mort'
FFTM = 'FFTM'
CBDT = 'CBDT'
CBLC = 'CBLC'
Meta = 'meta'
2024-05-03 21:02:58 +10:00
def __str__(self) -> str: return self._value_
2024-05-04 11:34:37 +10:00
def parse_table_tag(f: BinaryIO) -> TableTag:
return read_tag(f, TableTag)
2024-05-03 21:02:58 +10:00
@dataclass
class TableDirectoryEntry:
tableTag: TableTag
checkSum: int
offset: int
length: int
def parse_table_directory_entry(f: BinaryIO) -> TableDirectoryEntry:
2024-05-04 11:34:37 +10:00
tableTag = parse_table_tag(f)
2024-05-03 21:02:58 +10:00
checkSum = read_u32(f)
offset = read_u32(f)
length = read_u32(f)
return TableDirectoryEntry(tableTag, checkSum, offset, length)
@dataclass
class FontDirectory:
offset_table: OffsetTable
table_directory: List[TableDirectoryEntry]
2024-09-15 16:10:41 +10:00
def get_entry(self, table_tag: TableTag) -> Optional[TableDirectoryEntry]:
for entry in self.table_directory:
if entry.tableTag == table_tag:
return entry
return None
def has_entry(self, table_tag: TableTag) -> bool:
return self.get_entry(table_tag) is not None
2024-05-03 21:02:58 +10:00
def parse_font_directory(f: BinaryIO) -> FontDirectory:
offset_table = parse_offset_table(f)
table_directory_entries = [parse_table_directory_entry(f) for _ in range(offset_table.numTables)]
2024-09-15 16:10:41 +10:00
return FontDirectory(offset_table, table_directory_entries)
2024-05-03 21:02:58 +10:00
@dataclass
class CmapSubtable(Table, ABD):
format: int
@dataclass
class CmapSubtable_Format_0(CmapSubtable):
length: int
language: int # For Macintosh platform, this value is QuickDraw language ID +1 if language specific, 0 otherwise. This is true for all CmapSubtable formats
glyphIdArray: List[int]
# TODO: Other formats besides 4
@dataclass
class SubHeader:
pass
def parse_sub_header(f: BinaryIO) -> SubHeader:
assert False
@dataclass
class CmapSubtable_Format_2(CmapSubtable):
length: int
2024-09-15 16:10:41 +10:00
language: int # TODO: Make this an optional int
2024-05-03 21:02:58 +10:00
subHeaderKeys: List[int] # 256 elements
subHeaders: List[SubHeader]
glyphIndexArray: List[int]
@dataclass
class CmapSubtable_Format_4(CmapSubtable):
length: int
language: int
segCountX2: int
searchRange: int
entrySelector: int
rangeShift: int
endCode: List[int]
startCode: List[int]
idDelta: List[int]
idRangeOffset: List[int]
glyphIdArray: List[int]
@dataclass
class CmapSubtable_Format_6(CmapSubtable):
length: int
language: int
firstCode: int
entryCount: int
glyphIdArray: List[int]
@dataclass
class SequentialMapGroup:
startCharCode: int
endCharCode: int
startGlyphID: int
def parse_sequential_map_group(f: BinaryIO) -> SequentialMapGroup:
startCharCode = read_u32(f)
endCharCode = read_u32(f)
startGlyphID = read_u32(f)
return SequentialMapGroup(startCharCode, endCharCode, startGlyphID)
@dataclass
class CmapSubtable_Format_12(CmapSubtable):
length: int
language: int
numGroups: int
groups: List[SequentialMapGroup]
@dataclass
2024-05-04 11:34:37 +10:00
class UnicodeRangeRecord(Record):
2024-05-03 21:02:58 +10:00
startUnicodeValue: int
additionalCount: int
def __repr__(self) -> str:
2024-05-04 11:34:37 +10:00
return f"<U+{hex(self.startUnicodeValue)[2:]:0>4}" + (f"-U+{hex(self.startUnicodeValue+self.additionalCount)[2:]:0>4}" if self.additionalCount > 0 else '')+">"
2024-05-03 21:02:58 +10:00
def parse_unicode_range_record(f: BinaryIO) -> UnicodeRangeRecord:
startUnicodeValue = read_u24(f)
additionalCount = read_u8(f)
assert startUnicodeValue + additionalCount <= 0xFFFFFF, (startUnicodeValue, additionalCount, startUnicodeValue + additionalCount)
return UnicodeRangeRecord(startUnicodeValue, additionalCount)
@dataclass
class DefaultUVSTable(Table):
numUnicodeValueRanges: int
ranges: List[UnicodeRangeRecord]
def parse_default_UVS_table(f: BinaryIO) -> DefaultUVSTable:
numUnicodeValueRanges = read_u32(f)
ranges = [parse_unicode_range_record(f) for _ in range(numUnicodeValueRanges)]
return DefaultUVSTable(numUnicodeValueRanges, ranges)
@dataclass
2024-05-04 11:34:37 +10:00
class UVSMappingRecord(Record):
2024-05-03 21:02:58 +10:00
unicodeValue: int
glyphID: int
def parse_UVS_mapping_record(f: BinaryIO) -> UVSMappingRecord:
unicodeValue = read_u24(f)
glyphID = read_u16(f)
return UVSMappingRecord(unicodeValue, glyphID)
@dataclass
class NonDefaultUVSTable(Table):
numUVSMappings: int
uvsMappings: List[UVSMappingRecord]
def parse_non_default_UVS_table(f: BinaryIO) -> NonDefaultUVSTable:
numUVSMappings = read_u32(f)
uvsMappings = [parse_UVS_mapping_record(f) for _ in range(numUVSMappings)]
return NonDefaultUVSTable(numUVSMappings, uvsMappings)
@dataclass
2024-05-04 11:34:37 +10:00
class VariationSelectorRecord(Record):
2024-05-03 21:02:58 +10:00
varSelector: int
defaultUVS: Optional[DefaultUVSTable]
nonDefaultUVS: Optional[NonDefaultUVSTable]
def parse_variation_selector_record(f: BinaryIO, start_tell: int) -> VariationSelectorRecord:
varSelector = read_u24(f)
defaultUVSOffset = read_u32(f)
nonDefaultUVSOffset = read_u32(f)
with SaveTell(f):
defaultUVS = parse_at_optional_offset(f, start_tell, defaultUVSOffset, parse_default_UVS_table)
nonDefaultUVS = parse_at_optional_offset(f, start_tell, nonDefaultUVSOffset, parse_non_default_UVS_table)
return VariationSelectorRecord(varSelector, defaultUVS, nonDefaultUVS)
@dataclass
class CmapSubtable_Format_14(CmapSubtable):
length: int
numVarSelectorRecords: int
varSelector: List[VariationSelectorRecord]
class PlatformID(Enum):
Unicode = 0
Macintosh = 1
ISO = 2
Windows = 3
Custom = 4
def __str__(self) -> str:
return self._name_
def parse_cmap_subtable(f: BinaryIO, platformID: PlatformID) -> CmapSubtable:
start_tell = f.tell()
format = read_u16(f)
assert format in [0, 2, 4, 6, 8, 10, 12, 13, 14], f"Invalid format: {format}"
match format:
case 0:
length = read_u16(f)
language = read_u16(f)
if platformID != PlatformID.Macintosh: assert language == 0
glyphIdArray = [read_u8(f) for _ in range(256)]
return CmapSubtable_Format_0(format, length, language, glyphIdArray)
2024-05-03 21:02:58 +10:00
case 4:
length = read_u16(f)
language = read_u16(f)
if platformID != PlatformID.Macintosh: assert language == 0
segCountX2 = read_u16(f)
assert segCountX2 % 2 == 0
segCount = segCountX2//2
searchRange = read_u16(f)
assert searchRange == 2*2**floor(log2(segCount))
entrySelector = read_u16(f)
assert entrySelector == log2(searchRange//2)
rangeShift = read_u16(f)
assert rangeShift == segCountX2 - searchRange
endCode = [read_u16(f) for _ in range(segCount)]
assert read_u16(f) == 0, "Reserved"
startCode = [read_u16(f) for _ in range(segCount)]
idDelta = [read_i16(f) for _ in range(segCount)]
idRangeOffset = [read_u16(f) for _ in range(segCount)]
remaining_length = length - (f.tell() - start_tell)
assert remaining_length % 2 == 0 # remaining length is made up of u16's
glyphIdArray = [read_u16(f) for _ in range(remaining_length//2)]
return CmapSubtable_Format_4(format, length, language, segCountX2, searchRange, entrySelector, rangeShift, endCode, startCode, idDelta, idRangeOffset, glyphIdArray)
case 6:
length = read_u16(f)
language = read_u16(f)
if platformID != PlatformID.Macintosh: assert language == 0
firstCode = read_u16(f)
entryCount = read_u16(f)
glyphIdArray = [read_u16(f) for _ in range(entryCount)]
2024-09-15 16:10:41 +10:00
assert length-4<f.tell()-start_tell<=length, (f.tell()-start_tell, length)
f.seek(length-(f.tell()-start_tell))
2024-05-03 21:02:58 +10:00
return CmapSubtable_Format_6(format, length, language, firstCode, entryCount, glyphIdArray)
case 12:
assert read_u16(f) == 0, "Reserved"
length = read_u32(f)
language = read_u32(f) # idk why language needs to also be a u32??
if platformID != PlatformID.Macintosh: assert language == 0
numGroups = read_u32(f)
groups = [parse_sequential_map_group(f) for _ in range(numGroups)]
assert f.tell()-start_tell == length, (f.tell()-start_tell, length)
return CmapSubtable_Format_12(format, length, language, numGroups, groups)
case 14:
length = read_u32(f)
numVarSelectorRecords = read_u32(f)
varSelector = [parse_variation_selector_record(f, start_tell) for _ in range(numVarSelectorRecords)]
# assert f.tell()-start_tell == length, (numVarSelectorRecords, f.tell()-start_tell, length) # this will return false because tables/records stored with offsets to them aren't counted by python, but are in the `length` variable
return CmapSubtable_Format_14(format, length, numVarSelectorRecords, varSelector)
case _:
assert False, f"Unimplemented: format: {format}"
assert False, format
class EncodingID(ABE):
@abstractmethod
def get_code_point(self, char: str) -> Optional[int]:
assert False, f"Unimplemented: get_code_point for {repr(self)}"
2024-05-03 21:02:58 +10:00
class UnicodeEncodingID(EncodingID, Enum):
Unicode_1_0 = 0
Unicode_1_1 = 1
ISO_IEC_10646 = 2
Unicode_2_0_BMP_Only = 3
Unicode_2_0_Full = 4
UnicodeVariationSequences = 5
UnicodeFull = 6
def __str__(self) -> str:
return self._name_
def get_code_point(self, char: str) -> int | None:
return ord(char) # TODO: Actually validate that the codepoint is within range of these different versions of Unicode Encoding
match self:
case _: assert False, self
2024-05-03 21:02:58 +10:00
class MacintoshEncodingID(EncodingID, Enum):
Roman = 0
def __str__(self) -> str:
return self._name_
class ISOEncodingID(EncodingID, Enum):
pass
class WindowsEncodingID(EncodingID, Enum):
Symbol = 0
UnicodeBMP = 1
ShiftJIS = 2
PRC = 3
Big5 = 4
Wansung = 5
Johab = 6
2024-09-15 16:10:41 +10:00
UnicodeFull = 10
2024-05-03 21:02:58 +10:00
def __str__(self) -> str:
return self._name_
def get_code_point(self, char: str) -> int | None:
match self:
case self.UnicodeBMP:
code_point = ord(char)
if 0 <= code_point <= 0xFFFF: return code_point
return None
case self.UnicodeFull: return ord(char)
case _: assert False, self
2024-05-03 21:02:58 +10:00
@dataclass
class CustomEncodingID(EncodingID):
encodingID: int
def encoding_ID_cls_from_platform_ID(platformID: PlatformID) -> Callable[[int], EncodingID]:
match platformID:
case PlatformID.Unicode: return UnicodeEncodingID
case PlatformID.Macintosh: return MacintoshEncodingID
case PlatformID.Windows: return WindowsEncodingID
case _:
assert False, f"Unimplemented: platformID: {platformID}"
assert False, platformID
2024-05-04 11:34:37 +10:00
def parse_encoding_ID(f: BinaryIO, platformID: PlatformID) -> EncodingID:
return read_id(f, encoding_ID_cls_from_platform_ID(platformID))
2024-05-03 21:02:58 +10:00
2024-09-15 16:10:41 +10:00
# TODO: Finish this
2024-05-03 21:02:58 +10:00
def parse_string_with_encoding_ID(f: BinaryIO, length: int, encodingID: EncodingID) -> str:
bytes = f.read(length)
match encodingID:
case MacintoshEncodingID.Roman: return bytes.decode(encoding='mac_roman')
case WindowsEncodingID.UnicodeBMP: return bytes.decode(encoding='utf-16be')
case _:
assert False, f"Unimplemented: encodingID: {encodingID}"
assert False, encodingID
@dataclass
2024-05-04 11:34:37 +10:00
class EncodingRecord(Record):
2024-05-03 21:02:58 +10:00
platformID: PlatformID
encodingID: EncodingID
subtable: CmapSubtable
def parse_encoding_record(f: BinaryIO, start_tell:int) -> EncodingRecord:
2024-05-04 11:34:37 +10:00
platformID = read_id(f, PlatformID)
2024-05-03 21:02:58 +10:00
encodingID = parse_encoding_ID(f, platformID)
subtableOffset = read_u32(f)
with SaveTell(f):
subtable = parse_at_offset(f, start_tell, subtableOffset, lambda f: parse_cmap_subtable(f, platformID))
return EncodingRecord(platformID, encodingID, subtable)
@dataclass
class CmapTable(Table):
version: int
numTables: int
encodingRecords: List[EncodingRecord]
def parse_cmap_table(f: BinaryIO) -> CmapTable:
start_tell = f.tell()
version = read_u16(f)
assert version in [0]
numTables = read_u16(f)
encodingRecords = [parse_encoding_record(f, start_tell) for _ in range(numTables)]
return CmapTable(version, numTables, encodingRecords)
2024-09-15 16:10:41 +10:00
HEAD_TABLE_MAGIC = 0x5F0F3CF5
2024-05-03 21:02:58 +10:00
@dataclass
class HeadTable(Table):
majorVersion: int
minorVersion: int
fontRevision: float
checkSumAdjustment: int
flags: int
unitsPerEm: int
created: datetime
modified: datetime
xMin: int
yMin: int
xMax: int
yMax: int
macStyle: int
lowestRecPPEM: int
fontDirectionHint: int
indexToLocFormat: int
glyphDataFormat: int
def parse_head_table(f: BinaryIO) -> HeadTable:
majorVersion = read_u16(f)
assert majorVersion == 1
minorVersion = read_u16(f)
assert minorVersion in [0]
fontRevision = read_fixed(f)
checkSumAdjustment = read_u32(f)
2024-09-15 16:10:41 +10:00
assert read_u32(f) == HEAD_TABLE_MAGIC, "magicNumber"
2024-05-03 21:02:58 +10:00
flags = read_u16(f)
unitsPerEm = read_u16(f)
created = read_long_datetime(f)
modified = read_long_datetime(f)
xMin = read_i16(f)
yMin = read_i16(f)
xMax = read_i16(f)
yMax = read_i16(f)
macStyle = read_u16(f)
lowestRecPPEM = read_u16(f)
fontDirectionHint = read_i16(f)
indexToLocFormat = read_i16(f)
assert indexToLocFormat in [0, 1]
glyphDataFormat = read_i16(f)
assert glyphDataFormat == 0
# assert macStyle & 0x01 == os2.fsSelection & 0x20 # bit 0 and bit 5 must match
# assert macStyle & 0x02 == os2.fsSelection & 0x01 # bit 1 and bit 0 must match
return HeadTable(majorVersion, minorVersion, fontRevision, checkSumAdjustment, flags, unitsPerEm, created, modified, xMin, yMin, xMax, yMax, macStyle, lowestRecPPEM, fontDirectionHint, indexToLocFormat, glyphDataFormat)
@dataclass
class HheaTable(Table):
majorVersion: int
minorVersion: int
ascender: int
descender: int
lineGap: int
advanceWidthMax: int
minLeftSideBearing: int
minRightSideBearing: int
xMaxExtent: int
caretSlopeRise: int
caretSlopeRun: int
caretOffset: int
metricDataFormat: int
numberOfHMetrics: int
def parse_hhea_table(f: BinaryIO) -> HheaTable:
majorVersion = read_u16(f)
assert majorVersion == 1
minorVersion = read_u16(f)
assert minorVersion in [0]
ascender = read_i16(f)
descender = read_i16(f)
lineGap = read_i16(f)
advanceWidthMax = read_u16(f)
minLeftSideBearing = read_i16(f)
minRightSideBearing = read_i16(f)
xMaxExtent = read_i16(f)
caretSlopeRise = read_i16(f)
caretSlopeRun = read_i16(f)
caretOffset = read_i16(f)
for _ in range(4): assert read_i16(f) == 0, "Reserved"
metricDataFormat = read_i16(f)
assert metricDataFormat == 0
numberOfHMetrics = read_u16(f)
return HheaTable(majorVersion, minorVersion, ascender, descender, lineGap, advanceWidthMax, minLeftSideBearing, minRightSideBearing, xMaxExtent, caretSlopeRise, caretSlopeRun, caretOffset, metricDataFormat, numberOfHMetrics)
@dataclass
class LongHorMetric:
advanceWidth: int
lsb: int
def parse_long_hor_metric(f: BinaryIO) -> LongHorMetric:
advanceWidth = read_u16(f)
lsb = read_i16(f)
return LongHorMetric(advanceWidth, lsb)
@dataclass
class HmtxTable(Table):
hMetrics: List[LongHorMetric]
leftSideBearing: List[int]
def parse_hmtx_table(f: BinaryIO, numberOfHMetrics: int, numGlyphs: int) -> HmtxTable:
assert numberOfHMetrics <= numGlyphs
hMetrics = [parse_long_hor_metric(f) for _ in range(numberOfHMetrics)]
leftSideBearing = [read_i16(f) for _ in range(numGlyphs-numberOfHMetrics)]
return HmtxTable(hMetrics, leftSideBearing)
@dataclass
class MaxpTable(Table):
version: float
numGlyphs: int
@dataclass
class MaxpTable_Ver_0_5(MaxpTable): pass
@dataclass
class MaxpTable_Ver_1_0(MaxpTable_Ver_0_5):
maxPoints: int
maxContours: int
maxCompositePoints: int
maxCompositeContours: int
maxZones: int
maxTwilightPoints: int
maxStorage: int
maxFunctionDefs: int
maxInstructionDefs: int
maxStackElements: int
maxSizeOfInstructions: int
maxComponentElements: int
maxComponentDepth: int
def parse_maxp_table(f: BinaryIO) -> MaxpTable:
version = read_fixed_version(f)
assert version in [0.5, 1.0], f"Invalid version: {version}"
numGlyphs = read_u16(f)
if version == 0.5:
return MaxpTable_Ver_0_5(version, numGlyphs)
maxPoints = read_u16(f)
maxContours = read_u16(f)
maxCompositePoints = read_u16(f)
maxCompositeContours = read_u16(f)
maxZones = read_u16(f)
maxTwilightPoints = read_u16(f)
maxStorage = read_u16(f)
maxFunctionDefs = read_u16(f)
maxInstructionDefs = read_u16(f)
maxStackElements = read_u16(f)
maxSizeOfInstructions = read_u16(f)
maxComponentElements = read_u16(f)
maxComponentDepth = read_u16(f)
if version == 1.0:
return MaxpTable_Ver_1_0(version, numGlyphs, maxPoints, maxContours, maxCompositePoints, maxCompositeContours, maxZones, maxTwilightPoints, maxStorage, maxFunctionDefs, maxInstructionDefs, maxStackElements, maxSizeOfInstructions, maxComponentElements, maxComponentDepth)
assert False, f"Unimplemented: version: {version}"
@dataclass
class NameTable(Table, ABD):
format: int
class LanguageID(ABE): pass
class MacintoshLanguageID(LanguageID, Enum):
English = 0
2024-09-15 16:10:41 +10:00
French = 1
German = 2
Italian = 3
Dutch = 4
Swedish = 5
Spanish = 6
Danish = 7
Portuguese = 8
Norwegian = 9
Hebrew = 10
Japanese = 11
Arabic = 12
Finnish = 13
Greek = 14
Icelandic = 15
Maltese = 16
Turkish = 17
Croatian = 18
Chinese_Traditional = 19
Urdu = 20
Hindi = 21
Thai = 22
Korean = 23
Lithuanian = 24
Polish = 25
Hungarian = 26
Estonian = 27
Latvian = 28
Sami = 29
Faroese = 30
FarsiPersian = 31
Russian = 32
Chinese_Simplified = 33
Flemish = 34
IrishGaelic = 35
Albanian = 36
Romanian = 37
Czech = 38
Slovak = 39
Slovenian = 40
Yiddish = 41
Serbian = 42
Macedonian = 43
Bulgarian = 44
Ukrainian = 45
Byelorussian = 46
Uzbek = 47
Kazakh = 48
Azerbaijani_CyrillicScript = 49
Azerbaijani_ArabicScript = 50
Armenian = 51
Georgian = 52
Moldavian = 53
Kirghiz = 54
Tajiki = 55
Turkmen = 56
Mongolian_MongolianScript = 57
Mongolian_CyrillicScript = 58
Pashto = 59
Kurdish = 60
Kashmiri = 61
Sindhi = 62
Tibetan = 63
Nepali = 64
Sanskrit = 65
Marathi = 66
Bengali = 67
Assamese = 68
Gujarati = 69
Punjabi = 70
Oriya = 71
Malayalam = 72
Kannada = 73
Tamil = 74
Telugu = 75
Sinhalese = 76
Burmese = 77
Khmer = 78
Lao = 79
Vietnamese = 80
Indonesian = 81
Tagalong = 82
Malay_RomanScript = 83
Malay_ArabicScript = 84
Amharic = 85
Tigrinya = 86
Galla = 87
Somali = 88
Swahili = 89
Rundi = 91
KinyarwandaRuanda = 90
NyanjaChewa = 92
Malagasy = 93
Esperanto = 94
Welsh = 128
Basque = 129
Catalan = 130
Latin = 131
Quenchua = 132
Guarani = 133
Aymara = 134
Tatar = 135
Uighur = 136
Dzongkha = 137
Javanese_RomanScript = 138
Sundanese_RomanScript = 139
Galician = 140
Afrikaans = 141
Breton = 142
Inuktitut = 143
ScottishGaelic = 144
ManxGaelic = 145
IrishGaelic_WithDotAbove = 146
Tongan = 147
Greek_Polytonic = 148
Greenlandic = 149
Azerbaijani_RomanScript = 150
2024-05-03 21:02:58 +10:00
def __str__(self) -> str: return self._name_
class WindowsLanguageID(LanguageID, Enum):
Afrikaans_SouthAfrica = 0x0436
Albanian_Albania = 0x041C
Alsatian_France = 0x0484
Amharic_Ethiopia = 0x045E
Arabic_Algeria = 0x1401
Arabic_Bahrain = 0x3C01
Arabic_Egypt = 0x0C01
Arabic_Iraq = 0x0801
Arabic_Jordan = 0x2C01
Arabic_Kuwait = 0x3401
Arabic_Lebanon = 0x3001
Arabic_Libya = 0x1001
Arabic_Morocco = 0x1801
Arabic_Oman = 0x2001
Arabic_Qatar = 0x4001
Arabic_SaudiArabia = 0x0401
Arabic_Syria = 0x2801
Arabic_Tunisia = 0x1C01
Arabic_UAE = 0x3801
Arabic_Yemen = 0x2401
Armenian_Armenia = 0x042B
Assamese_India = 0x044D
Azeri_Cyrillic_Azerbaijan = 0x082C
Azeri_Latin_Azerbaijan = 0x042C
Bashkir_Russia = 0x046D
Basque_Basque = 0x042D
Belarusian_Belarus = 0x0423
Bengali_Bangladesh = 0x0845
Bengali_India = 0x0445
Bosnian_Cyrillic_BosniaAndHerzegovina = 0x201A
Bosnian_Latin_BosniaAndHerzegovina = 0x141A
Breton_France = 0x047E
Bulgarian_Bulgaria = 0x0402
Catalan_Catalan = 0x0403
Chinese_HongKongSAR = 0x0C04
Chinese_MacaoSAR = 0x1404
Chinese_PeoplesRepublicOfChina = 0x0804
Chinese_Singapore = 0x1004
Chinese_Taiwan = 0x0404
Corsican_France = 0x0483
Croatian_Croatia = 0x041A
Croatian_Latin_BosniaAndHerzegovina = 0x101A
Czech_CzechRepublic = 0x0405
Danish_Denmark = 0x0406
Dari_Afghanistan = 0x048C
Divehi_Maldives = 0x0465
Dutch_Belgium = 0x0813
Dutch_Netherlands = 0x0413
English_Australia = 0x0C09
English_Belize = 0x2809
English_Canada = 0x1009
English_Caribbean = 0x2409
English_India = 0x4009
English_Ireland = 0x1809
English_Jamaica = 0x2009
English_Malaysia = 0x4409
English_NewZealand = 0x1409
English_RepublicOfThePhilippines = 0x3409
English_Singapore = 0x4809
English_SouthAfrica = 0x1C09
English_TrinidadAndTobago = 0x2C09
English_UnitedKingdom = 0x0809
English_UnitedStates = 0x0409
English_Zimbabwe = 0x3009
Estonian_Estonia = 0x0425
Faroese_FaroeIslands = 0x0438
Filipino_Philippines = 0x0464
Finnish_Finland = 0x040B
French_Belgium = 0x080C
French_Canada = 0x0C0C
French_France = 0x040C
French_Luxembourg = 0x140c
French_PrincipalityOfMonaco = 0x180C
French_Switzerland = 0x100C
Frisian_Netherlands = 0x0462
Galician_Galician = 0x0456
Georgian_Georgia = 0x0437
German_Austria = 0x0C07
German_Germany = 0x0407
German_Liechtenstein = 0x1407
German_Luxembourg = 0x1007
German_Switzerland = 0x0807
Greek_Greece = 0x0408
Greenlandic_Greenland = 0x046F
Gujarati_India = 0x0447
Hausa_Latin_Nigeria = 0x0468
Hebrew_Israel = 0x040D
Hindi_India = 0x0439
Hungarian_Hungary = 0x040E
Icelandic_Iceland = 0x040F
Igbo_Nigeria = 0x0470
Indonesian_Indonesia = 0x0421
Inuktitut_Canada = 0x045D
Inuktitut_Latin_Canada = 0x085D
Irish_Ireland = 0x083C
isiXhosa_SouthAfrica = 0x0434
isiZulu_SouthAfrica = 0x0435
Italian_Italy = 0x0410
Italian_Switzerland = 0x0810
Japanese_Japan = 0x0411
Kannada_India = 0x044B
Kazakh_Kazakhstan = 0x043F
Khmer_Cambodia = 0x0453
Kiche_Guatemala = 0x0486
Kinyarwanda_Rwanda = 0x0487
Kiswahili_Kenya = 0x0441
Konkani_India = 0x0457
Korean_Korea = 0x0412
Kyrgyz_Kyrgyzstan = 0x0440
Lao_LaoPDR = 0x0454
Latvian_Latvia = 0x0426
Lithuanian_Lithuania = 0x0427
LowerSorbian_Germany = 0x082E
Luxembourgish_Luxembourg = 0x046E
Macedonian_FYROM_FormerYugoslavRepublicOfMacedonia = 0x042F
Malay_BruneiDarussalam = 0x083E
Malay_Malaysia = 0x043E
Malayalam_India = 0x044C
Maltese_Malta = 0x043A
Maori_NewZealand = 0x0481
Mapudungun_Chile = 0x047A
Marathi_India = 0x044E
Mohawk_Mohawk = 0x047C
Mongolian_Cyrillic_Mongolia = 0x0450
Mongolian_Traditional_PeoplesRepublicOfChina = 0x0850
Nepali_Nepal = 0x0461
Norwegian_Bokmal_Norway = 0x0414
Norwegian_Nynorsk_Norway = 0x0814
Occitan_France = 0x0482
Odia_formerlyOriya_India = 0x0448
Pashto_Afghanistan = 0x0463
Polish_Poland = 0x0415
Portuguese_Brazil = 0x0416
Portuguese_Portugal = 0x0816
Punjabi_India = 0x0446
Quechua_Bolivia = 0x046B
Quechua_Ecuador = 0x086B
Quechua_Peru = 0x0C6B
Romanian_Romania = 0x0418
Romansh_Switzerland = 0x0417
Russian_Russia = 0x0419
Sami_Inari_Finland = 0x243B
Sami_Lule_Norway = 0x103B
Sami_Lule_Sweden = 0x143B
Sami_Northern_Finland = 0x0C3B
Sami_Northern_Norway = 0x043B
Sami_Northern_Sweden = 0x083B
Sami_Skolt_Finland = 0x203B
Sami_Southern_Norway = 0x183B
Sami_Southern_Sweden = 0x1C3B
Sanskrit_India = 0x044F
Serbian_Cyrillic_BosniaAndHerzegovina = 0x1C1A
Serbian_Cyrillic_Serbia = 0x0C1A
Serbian_Latin_BosniaAndHerzegovina = 0x181A
Serbian_Latin_Serbia = 0x081A
SesothoSaLeboa_SouthAfrica = 0x046C
Setswana_SouthAfrica = 0x0432
Sinhala_SriLanka = 0x045B
Slovak_Slovakia = 0x041B
Slovenian_Slovenia = 0x0424
Spanish_Argentina = 0x2C0A
Spanish_Bolivia = 0x400A
Spanish_Chile = 0x340A
Spanish_Colombia = 0x240A
Spanish_CostaRica = 0x140A
Spanish_DominicanRepublic = 0x1C0A
Spanish_Ecuador = 0x300A
Spanish_ElSalvador = 0x440A
Spanish_Guatemala = 0x100A
Spanish_Honduras = 0x480A
Spanish_Mexico = 0x080A
Spanish_Nicaragua = 0x4C0A
Spanish_Panama = 0x180A
Spanish_Paraguay = 0x3C0A
Spanish_Peru = 0x280A
Spanish_PuertoRico = 0x500A
Spanish_ModernSort_Spain = 0x0C0A
Spanish_TraditionalSort_Spain = 0x040A
Spanish_UnitedStates = 0x540A
Spanish_Uruguay = 0x380A
Spanish_Venezuela = 0x200A
Sweden_Finland = 0x081D
Swedish_Sweden = 0x041D
Syriac_Syria = 0x045A
Tajik_Cyrillic_Tajikistan = 0x0428
Tamazight_Latin_Algeria = 0x085F
Tamil_India = 0x0449
Tatar_Russia = 0x0444
Telugu_India = 0x044A
Thai_Thailand = 0x041E
Tibetan_PRC = 0x0451
Turkish_Turkey = 0x041F
Turkmen_Turkmenistan = 0x0442
Uighur_PRC = 0x0480
Ukrainian_Ukraine = 0x0422
UpperSorbian_Germany = 0x042E
Urdu_IslamicRepublicOfPakistan = 0x0420
Uzbek_Cyrillic_Uzbekistan = 0x0843
Uzbek_Latin_Uzbekistan = 0x0443
Vietnamese_Vietnam = 0x042A
Welsh_UnitedKingdom = 0x0452
Wolof_Senegal = 0x0488
Yakut_Russia = 0x0485
Yi_PRC = 0x0478
Yoruba_Nigeria = 0x046A
def __str__(self) -> str: return self._name_
def languageID_cls_from_platform_ID(platformID: PlatformID) -> Callable[[int], LanguageID]:
match platformID:
case PlatformID.Macintosh: return MacintoshLanguageID
case PlatformID.Windows: return WindowsLanguageID
case _:
assert False, f"Unimplemented: platformID: {platformID}"
assert False, platformID
def parse_language_ID(f: BinaryIO, platformID: PlatformID) -> LanguageID:
2024-05-04 11:34:37 +10:00
return read_id(f, languageID_cls_from_platform_ID(platformID))
2024-05-03 21:02:58 +10:00
class NameID(ABE): pass
class PredefinedNameID(NameID, Enum):
COPYRIGHT_NOTICE = 0
FAMILY = 1
SUBFAMILY = 2
UNIQUE_ID = 3
FULL_NAME = 4
VERSION = 5
POST_SCRIPT_NAME = 6
TRADEMARK = 7
MANUFACTURER = 8
DESIGNER = 9
DESCRIPTION = 10
VENDOR_URL = 11
DESIGNER_URL = 12
LICENSE = 13
LICENSE_URL = 14
# RESERVED
TYPOGRAPHIC_FAMILY = 16
TYPOGRAPHIC_SUBFAMILY = 17
COMPATIBLE_FULL = 18
SAMPLE_TEXT = 19
POST_SCRIPT_CID = 20
WWS_FAMILY = 21
WWS_SUBFAMILY = 22
LIGHT_BACKGROUND_PALETTE = 23
DARK_BACKGROUND_PALETTE = 24
VARIATIONS_POST_SCRIPT_NAME_PREFIX = 25
@dataclass
class FontSpecificNameID(NameID):
nameID: int
def parse_name_ID(f: BinaryIO) -> NameID:
2024-05-04 11:34:37 +10:00
return read_id_from_ranges(f, (255, PredefinedNameID), (32767, FontSpecificNameID), umbrellaIdCls=NameID)
2024-05-03 21:02:58 +10:00
@dataclass
class NameRecord:
platformID: PlatformID
encodingID: EncodingID
languageID: LanguageID
nameID: NameID
length: int
string: str
def parse_name_record(f: BinaryIO, stringBytes: BinaryIO) -> NameRecord:
2024-05-04 11:34:37 +10:00
platformID = read_id(f, PlatformID)
2024-05-03 21:02:58 +10:00
encodingID = parse_encoding_ID(f, platformID)
languageID = parse_language_ID(f, platformID)
nameID = parse_name_ID(f)
length = read_u16(f)
offset = read_u16(f)
stringBytes.seek(offset)
string = parse_string_with_encoding_ID(stringBytes, length, encodingID)
return NameRecord(platformID, encodingID, languageID, nameID, length, string)
@dataclass
class NameTable_Format_0(NameTable):
count: int
nameRecord: List[NameRecord]
2024-09-15 16:10:41 +10:00
def parse_name_table(f: BinaryIO, length: int) -> NameTable:
2024-05-03 21:02:58 +10:00
start_tell = f.tell()
format = read_u16(f)
assert format in [0, 1]
match format:
case 0:
count = read_u16(f)
stringOffset = read_u16(f)
stringLength = length-stringOffset
with SaveTell(f):
stringBytes = parse_at_offset(f, start_tell, stringOffset, lambda f: BytesIO(f.read(stringLength)))
nameRecord = [parse_name_record(f, stringBytes) for _ in range(count)]
return NameTable_Format_0(format, count, nameRecord)
case _:
assert False, f"Unimplemented: format: {format}"
2024-09-15 16:10:41 +10:00
assert False, format
2024-05-03 21:02:58 +10:00
@dataclass
class VendorTag:
achVendID: str
2024-05-04 11:34:37 +10:00
def parse_vendor_tag(f: BinaryIO) -> VendorTag:
return read_tag(f, VendorTag, strict=False)
2024-05-03 21:02:58 +10:00
@dataclass
class OS2Table(Table, ABD):
version: int
xAvgCharWidth: int
usWeightClass: int
usWidthClass: int
fsType: int
ySubscriptXSize: int
ySubscriptYSize: int
ySubscriptXOffset: int
ySubscriptYOffset: int
ySuperscriptXSize: int
ySuperscriptYSize: int
ySuperscriptXOffset: int
ySuperscriptYOffset: int
yStrikeoutSize: int
yStrikeoutPosition: int
sFamilyClass: int
panose: Tuple[int, int, int, int, int, int, int, int, int, int]
ulUnicodeRange1: int
ulUnicodeRange2: int
ulUnicodeRange3: int
ulUnicodeRange4: int
achVendID: VendorTag
fsSelection: int
usFirstCharIndex: int
usLastCharIndex: int
sTypoAscender: int
sTypoDescender: int
sTypoLineGap: int
usWinAscent: int
usWinDescent: int
2024-09-15 16:10:41 +10:00
def __post_init__(self):
if self.fsSelection & 0x40: assert self.fsSelection & (0b100001) == 0 # bit 6 indicates that the font is regular, and therefore cannot be bold or italic.
assert self.fsSelection & 0xfc00 == 0, "reserved"
2024-05-03 21:02:58 +10:00
@dataclass
class OS2Table_Ver_0(OS2Table): pass
@dataclass
class OS2Table_Ver_1(OS2Table_Ver_0):
ulCodePageRange1: int
ulCodePageRange2: int
@dataclass
class OS2Table_Ver_2(OS2Table_Ver_1):
sxHeight: int
sCapHeight: int
usDefaultChar: int
usBreakChar: int
usMaxContext: int
@dataclass
class OS2Table_Ver_3(OS2Table_Ver_2): pass
@dataclass
class OS2Table_Ver_4(OS2Table_Ver_3): pass
@dataclass
class OS2Table_Ver_5(OS2Table_Ver_4):
usLowerOpticalPointSize: int
usUpperOpticalPointSize: int
def parse_OS2_table(f: BinaryIO) -> OS2Table:
version = read_u16(f)
assert version in [0, 1, 2, 3, 4, 5]
xAvgCharWidth = read_i16(f)
usWeightClass, usWidthClass = read_u16(f), read_u16(f)
fsType = read_u16(f)
ySubscriptXSize, ySubscriptYSize, ySubscriptXOffset, ySubscriptYOffset, ySuperscriptXSize, ySuperscriptYSize, ySuperscriptXOffset, ySuperscriptYOffset, yStrikeoutSize, yStrikeoutPosition = [read_i16(f) for _ in range(10)]
sFamilyClass = read_i16(f)
panose0, panose1, panose2, panose3, panose4, panose5, panose6, panose7, panose8, panose9 = [read_u8(f) for _ in range(10)]
panose = (panose0, panose1, panose2, panose3, panose4, panose5, panose6, panose7, panose8, panose9)
ulUnicodeRange1, ulUnicodeRange2, ulUnicodeRange3, ulUnicodeRange4 = [read_u32(f) for _ in range(4)]
2024-05-04 11:34:37 +10:00
achVendID = parse_vendor_tag(f)
2024-05-03 21:02:58 +10:00
fsSelection = read_u16(f)
usFirstCharIndex, usLastCharIndex = read_u16(f), read_u16(f)
sTypoAscender, sTypoDescender, sTypoLineGap = read_i16(f), read_i16(f), read_i16(f)
usWinAscent, usWinDescent = read_u16(f), read_u16(f)
if version == 0:
return OS2Table_Ver_0(version, xAvgCharWidth, usWeightClass, usWidthClass, fsType, ySubscriptXSize, ySubscriptYSize, ySubscriptXOffset, ySubscriptYOffset, ySuperscriptXSize, ySuperscriptYSize, ySuperscriptXOffset, ySuperscriptYOffset, yStrikeoutSize, yStrikeoutPosition, sFamilyClass, panose, ulUnicodeRange1, ulUnicodeRange2, ulUnicodeRange3, ulUnicodeRange4, achVendID, fsSelection, usFirstCharIndex, usLastCharIndex, sTypoAscender, sTypoDescender, sTypoLineGap, usWinAscent, usWinDescent)
ulCodePageRange1, ulCodePageRange2 = read_u32(f), read_u32(f)
if version == 1:
return OS2Table_Ver_1(version, xAvgCharWidth, usWeightClass, usWidthClass, fsType, ySubscriptXSize, ySubscriptYSize, ySubscriptXOffset, ySubscriptYOffset, ySuperscriptXSize, ySuperscriptYSize, ySuperscriptXOffset, ySuperscriptYOffset, yStrikeoutSize, yStrikeoutPosition, sFamilyClass, panose, ulUnicodeRange1, ulUnicodeRange2, ulUnicodeRange3, ulUnicodeRange4, achVendID, fsSelection, usFirstCharIndex, usLastCharIndex, sTypoAscender, sTypoDescender, sTypoLineGap, usWinAscent, usWinDescent, ulCodePageRange1, ulCodePageRange2)
sxHeight, sCapHeight = read_i16(f), read_i16(f)
usDefaultChar, usBreakChar, usMaxContext = read_u16(f), read_u16(f), read_u16(f)
if version == 2:
return OS2Table_Ver_2(version, xAvgCharWidth, usWeightClass, usWidthClass, fsType, ySubscriptXSize, ySubscriptYSize, ySubscriptXOffset, ySubscriptYOffset, ySuperscriptXSize, ySuperscriptYSize, ySuperscriptXOffset, ySuperscriptYOffset, yStrikeoutSize, yStrikeoutPosition, sFamilyClass, panose, ulUnicodeRange1, ulUnicodeRange2, ulUnicodeRange3, ulUnicodeRange4, achVendID, fsSelection, usFirstCharIndex, usLastCharIndex, sTypoAscender, sTypoDescender, sTypoLineGap, usWinAscent, usWinDescent, ulCodePageRange1, ulCodePageRange2, sxHeight, sCapHeight, usDefaultChar, usBreakChar, usMaxContext)
if version == 3:
return OS2Table_Ver_3(version, xAvgCharWidth, usWeightClass, usWidthClass, fsType, ySubscriptXSize, ySubscriptYSize, ySubscriptXOffset, ySubscriptYOffset, ySuperscriptXSize, ySuperscriptYSize, ySuperscriptXOffset, ySuperscriptYOffset, yStrikeoutSize, yStrikeoutPosition, sFamilyClass, panose, ulUnicodeRange1, ulUnicodeRange2, ulUnicodeRange3, ulUnicodeRange4, achVendID, fsSelection, usFirstCharIndex, usLastCharIndex, sTypoAscender, sTypoDescender, sTypoLineGap, usWinAscent, usWinDescent, ulCodePageRange1, ulCodePageRange2, sxHeight, sCapHeight, usDefaultChar, usBreakChar, usMaxContext)
if version == 4:
return OS2Table_Ver_4(version, xAvgCharWidth, usWeightClass, usWidthClass, fsType, ySubscriptXSize, ySubscriptYSize, ySubscriptXOffset, ySubscriptYOffset, ySuperscriptXSize, ySuperscriptYSize, ySuperscriptXOffset, ySuperscriptYOffset, yStrikeoutSize, yStrikeoutPosition, sFamilyClass, panose, ulUnicodeRange1, ulUnicodeRange2, ulUnicodeRange3, ulUnicodeRange4, achVendID, fsSelection, usFirstCharIndex, usLastCharIndex, sTypoAscender, sTypoDescender, sTypoLineGap, usWinAscent, usWinDescent, ulCodePageRange1, ulCodePageRange2, sxHeight, sCapHeight, usDefaultChar, usBreakChar, usMaxContext)
usLowerOpticalPointSize, usUpperOpticalPointSize = read_u16(f), read_u16(f)
if version == 5:
return OS2Table_Ver_5(version, xAvgCharWidth, usWeightClass, usWidthClass, fsType, ySubscriptXSize, ySubscriptYSize, ySubscriptXOffset, ySubscriptYOffset, ySuperscriptXSize, ySuperscriptYSize, ySuperscriptXOffset, ySuperscriptYOffset, yStrikeoutSize, yStrikeoutPosition, sFamilyClass, panose, ulUnicodeRange1, ulUnicodeRange2, ulUnicodeRange3, ulUnicodeRange4, achVendID, fsSelection, usFirstCharIndex, usLastCharIndex, sTypoAscender, sTypoDescender, sTypoLineGap, usWinAscent, usWinDescent, ulCodePageRange1, ulCodePageRange2, sxHeight, sCapHeight, usDefaultChar, usBreakChar, usMaxContext, usLowerOpticalPointSize, usUpperOpticalPointSize)
assert False, f"Unimplemented: version: {version}"
# TODO: Maybe make this use formats instead?
@dataclass
class PostTable(Table, ABD):
"""
NOTE: Different versions (even among minor versions) are not necessarily backwards compatible
"""
version: float
italicAngle: float
underlinePosition: int
underlineThickness: int
isFixedPitch: int
minMemType42: int
maxMemType42: int
minMemType1: int
maxMemType1: int
@dataclass
class PostTable_Ver_1_0(PostTable): pass
@dataclass
class PostTable_Ver_2_0(PostTable):
numGlyphs: int
glyphNameIndex: List[int]
names: List[str]
@dataclass
class PostTable_Ver_3_0(PostTable): pass
def parse_post_table(f: BinaryIO, length: int) -> PostTable:
start_tell = f.tell()
version = read_fixed_version(f)
assert version in [1.0, 2.0, 2.5, 3.0, 4.0] # Apple documentation says that there is a version 4.0 (format4)
italicAngle = read_fixed(f)
underlinePosition = read_i16(f)
underlineThickness = read_i16(f)
isFixedPitch = read_u32(f)
minMemType42 = read_u32(f)
maxMemType42 = read_u32(f)
minMemType1 = read_u32(f)
maxMemType1 = read_u32(f)
match version:
case 1.0:
assert False # TODO: I think that that's it?
case 2.0:
numGlyphs = read_u16(f)
glyphNameIndex = [read_u16(f) for _ in range(numGlyphs)]
names: List[str] = []
while length - (f.tell() - start_tell) > 0: # kinda dangerous, but I think it's the only way to make it work? number of strings is not necessarily equal to numGlyphs. I think that you could probably figure out the number of strings by filtering elements in the glyphNameIndex array
names.append(read_pascal_string(f))
2024-09-15 16:10:41 +10:00
assert f.tell()-start_tell == length
2024-05-03 21:02:58 +10:00
return PostTable_Ver_2_0(version, italicAngle, underlinePosition, underlineThickness, isFixedPitch, minMemType42, maxMemType42, minMemType1, maxMemType1, numGlyphs, glyphNameIndex, names)
case 3.0:
return PostTable_Ver_3_0(version, italicAngle, underlinePosition, underlineThickness, isFixedPitch, minMemType42, maxMemType42, minMemType1, maxMemType1)
case _:
assert False, f"Unimplemented: version: {version}"
assert False, version
# 5.5
@dataclass
class SvgTable(Table):
pass
def parse_svg_table(f: BinaryIO) -> SvgTable:
assert False
# 5.6
@dataclass
class SignatureBlock(ABD): pass
@dataclass
class SignatureBlock_Format_1(SignatureBlock):
signatureLength: int
signature: bytes # PKCS#7 packet
def parse_signature_block(f: BinaryIO, format: int, length: int) -> SignatureBlock:
assert format in [1]
start_tell = f.tell()
match format:
case 1:
assert read_u16(f) == 0, "Reserved1"
assert read_u16(f) == 0, "Reserved2"
signatureLength = read_u32(f)
signature = f.read(signatureLength)
assert f.tell()-start_tell == length, (f.tell()-start_tell, length)
return SignatureBlock_Format_1(signatureLength, signature)
case _:
assert False, f"Unimplemented: format: {format}"
assert False, format
@dataclass
class SignatureRecord:
format: int
length: int
signatureBlock: SignatureBlock
def parse_signature_record(f: BinaryIO, start_tell: int) -> SignatureRecord:
format = read_u32(f)
length = read_u32(f)
signatureBlockOffset = read_u32(f)
with SaveTell(f):
signatureBlock = parse_at_offset(f, start_tell, signatureBlockOffset, lambda f: parse_signature_block(f, format, length))
return SignatureRecord(format, length, signatureBlock)
@dataclass
class DSIGTable(Table, ABD):
version: int # for some reasons it's u32?
numSignatures: int
flags: int # It's u16 but only bits 0-7 are documented?
signatureRecords: List[SignatureRecord] # there's a typo in the ISO documentation.
2024-09-15 16:10:41 +10:00
def __post_init__(self):
assert self.flags & 0xfe == 0, "Reserved"
2024-05-03 21:02:58 +10:00
@dataclass
class DSIGTable_Ver_1(DSIGTable): pass
def parse_DSIG_table(f: BinaryIO) -> DSIGTable:
start_tell = f.tell()
version = read_u32(f)
assert version in [1]
numSignatures = read_u16(f)
flags = read_u16(f)
signatureRecords = [parse_signature_record(f, start_tell) for _ in range(numSignatures)]
if version == 1:
return DSIGTable_Ver_1(version, numSignatures, flags, signatureRecords)
assert False, f"Unimplemented: version: {version}"
@dataclass
class DeviceRecord:
# format: int # format seems to be a constant 0?
pixelSize: int
maxWidth: int
widths: List[int]
def parse_device_record(f: BinaryIO, size: int, numGlyphs: int) -> DeviceRecord:
start_tell = f.tell()
pixelSize = read_u8(f)
maxWidth = read_u8(f)
widths = [read_u8(f) for _ in range(numGlyphs)]
# long aligning
assert size-4<f.tell()-start_tell<=size
f.seek(start_tell+size)
return DeviceRecord(pixelSize, maxWidth, widths)
@dataclass
class HdmxTable(Table):
version: int
numRecords: int
sizeDeviceRecord: int
records: List[DeviceRecord]
def parse_hdmx_table(f: BinaryIO, numGlyphs: int) -> HdmxTable:
version = read_u16(f)
assert version in [0]
numRecords = read_i16(f) # I don't know why numRecords can be negative?
sizeDeviceRecord = read_i32(f) # Similarly, why can a size be negative?
records = [parse_device_record(f, sizeDeviceRecord, numGlyphs) for _ in range(numRecords)]
return HdmxTable(version, numRecords, sizeDeviceRecord, records)
@dataclass
class KernSubtableCoverage:
bytes: int
def horizontal(self) -> int: return self.bytes & 1
def minimum(self) -> int: return self.bytes >> 1 & 1
def crossstream(self) -> int: return self.bytes >> 2 & 1
def override(self) -> int: return self.bytes >> 3 & 1
def format(self) -> int: return self.bytes >> 8 & 0x1000
def __post_init__(self):
assert self.bytes >> 4 & 0x10 == 0, self.bytes
assert self.format() in [0, 2]
@dataclass
class KernSubtable(Table, ABD):
version: int
length: int
coverage: KernSubtableCoverage
@dataclass
class KerningPairValue:
left: int
right: int
value: float
def parse_kerning_pair_value(f: BinaryIO) -> KerningPairValue:
left = read_u16(f)
right = read_u16(f)
value = read_i16(f)
return KerningPairValue(left, right, value)
@dataclass
class KernSubtable_Format_0(KernSubtable):
nPairs: int
searchRange: int
entrySelector: int
rangeShift: int
pairs: List[KerningPairValue]
def parse_kern_subtable(f: BinaryIO) -> KernSubtable:
start_tell = f.tell()
version = read_u16(f)
assert version in [0]
length = read_u16(f)
coverage = KernSubtableCoverage(read_u16(f))
match coverage.format():
case 0:
nPairs = read_u16(f)
searchRange = read_u16(f)
entrySelector = read_u16(f)
rangeShift = read_u16(f)
pairs = [parse_kerning_pair_value(f) for _ in range(nPairs)]
assert f.tell() - start_tell == length, (f.tell(), start_tell, length)
return KernSubtable_Format_0(version, length, coverage, nPairs, searchRange, entrySelector, rangeShift, pairs)
case 2:
assert False
case _:
assert False, f"Unimplemented: format: {coverage.format()}"
assert False, (version, length, coverage)
2024-05-03 21:02:58 +10:00
@dataclass
class KernTable(Table):
version: int
nTables: int
subtables: List[KernSubtable]
2024-05-03 21:02:58 +10:00
def parse_Kern_table(f: BinaryIO) -> KernTable:
version = read_u16(f)
assert version in [0]
nTables = read_u16(f)
subtables = [parse_kern_subtable(f) for _ in range(nTables)]
return KernTable(version, nTables, subtables)
2024-05-03 21:02:58 +10:00
@dataclass
class LTSHTable(Table):
version: int
numGlyphs: int
yPels: List[int]
@dataclass
class LTSHTable_Ver_0(LTSHTable): pass
2024-05-03 21:02:58 +10:00
def parse_LTSH_table(f: BinaryIO) -> LTSHTable:
version = read_u16(f)
assert version in [0]
numGlyphs = read_u16(f)
yPels = [read_u8(f) for _ in range(numGlyphs)]
if version == 0:
return LTSHTable_Ver_0(version, numGlyphs, yPels)
assert False, f"Unimplemented: version: {version}"
2024-05-03 21:02:58 +10:00
@dataclass
class PCLTTable(Table):
pass
def parse_PCLT_table(f: BinaryIO) -> PCLTTable:
assert False
@dataclass
class RatioRangeRecord(Record):
bCharSet: int
xRatio: int
yStartRatio: int
yEndRatio: int
def parse_ratio_range_record(f: BinaryIO) -> RatioRangeRecord:
bCharSet = read_u8(f)
xRatio = read_u8(f)
yStartRatio = read_u8(f)
yEndRatio = read_u8(f)
return RatioRangeRecord(bCharSet, xRatio, yStartRatio, yEndRatio)
@dataclass
class VTableRecord(Record):
yPelHeight: int
yMax: int
yMin: int
def parse_v_table_record(f: BinaryIO) -> VTableRecord:
yPelHeight = read_u16(f)
yMax = read_i16(f)
yMin = read_i16(f)
return VTableRecord(yPelHeight, yMax, yMin)
@dataclass
class VDMXGroup:
recs: int
startsz: int
endsz: int
entry: List[VTableRecord]
def parse_vdmx_group(f: BinaryIO) -> VDMXGroup:
recs = read_u16(f)
startsz = read_u8(f)
endsz = read_u8(f)
entry = [parse_v_table_record(f) for _ in range(recs)]
return VDMXGroup(recs, startsz, endsz, entry)
2024-05-03 21:02:58 +10:00
@dataclass
class VDMXTable(Table):
version: int
numRecs: int
numRatios: int
ratRange: List[RatioRangeRecord]
groups_by_ratio: List[VDMXGroup] # same objects as in groups, but ordered by which ratio references them
groups: List[VDMXGroup]
2024-05-03 21:02:58 +10:00
def parse_VDMX_table(f: BinaryIO) -> VDMXTable:
start_tell = f.tell()
version = read_u16(f)
assert version in [0, 1]
numRecs = read_u16(f)
numRatios = read_u16(f)
ratRange = [parse_ratio_range_record(f) for _ in range(numRecs)]
offsets = [read_u16(f) for _ in range(numRatios)]
groups_by_ratio, groups = parse_list_and_use_offsets_into(f, start_tell, offsets, numRecs, parse_vdmx_group)
return VDMXTable(version, numRecs, numRatios, ratRange, groups_by_ratio, groups)
2024-05-03 21:02:58 +10:00
# NOTE: Different versions of VheaTable are not backwards compatible due to differences in three fields
@dataclass
class VheaTable(Table):
version: float
# ascent / vertTypoAscender
# descent / vertTypoDescender
# lineGap / vertTypoLineGap
advanceHeightMax: int
minTopSideBearing: int
minBottomSideBearing: int
yMaxExtent: int
caretSlopeRise: int
caretSlopeRun: int
caretOffset: int
metricDataFormat: int
numOfLongVerMetris: int
@dataclass
class VheaTable_Ver_1_0(VheaTable):
ascent: int
descent: int
lineGap: int
@dataclass
class VheaTable_Ver_1_1(VheaTable):
vertTypeAscender: int
vertTypoDescender: int
vertTypoLineGap: int
def parse_vhea_table(f: BinaryIO) -> VheaTable:
version = read_fixed_version(f)
assert version in [1.0, 1.1], f"Invalid version: {version}"
match version:
case 1.0:
ascent = read_i16(f)
descent = read_i16(f)
lineGap = read_i16(f)
assert lineGap == 0, "Reserved"
case 1.1:
vertTypeAscender = read_i16(f)
vertTypoDescender = read_i16(f)
vertTypoLineGap = read_i16(f)
case _:
assert False, f"Unimplemented: version: {version}"
advanceHeightMax = read_i16(f)
minTopSideBearing = read_i16(f)
minBottomSideBearing = read_i16(f)
yMaxExtent = read_i16(f)
caretSlopeRise = read_i16(f)
caretSlopeRun = read_i16(f)
caretOffset = read_i16(f)
for _ in range(4): assert read_i16(f) == 0, "Reserved"
metricDataFormat = read_i16(f)
assert metricDataFormat == 0
numOfLongVerMetris = read_u16(f)
if version == 1.0:
return VheaTable_Ver_1_0(version, advanceHeightMax, minTopSideBearing, minBottomSideBearing, yMaxExtent, caretSlopeRise, caretSlopeRun, caretOffset, metricDataFormat, numOfLongVerMetris, ascent, descent, lineGap)
if version == 1.1:
return VheaTable_Ver_1_1(version, advanceHeightMax, minTopSideBearing, minBottomSideBearing, yMaxExtent, caretSlopeRise, caretSlopeRun, caretOffset, metricDataFormat, numOfLongVerMetris, vertTypeAscender, vertTypoDescender, vertTypoLineGap)
assert False, f"Unimplemented: version: {version}"
@dataclass
class VerticalMetricsEntry:
advanceHeight: int
topSideBearing: int
def parse_vertical_metrics_entry(f: BinaryIO) -> VerticalMetricsEntry:
advanceHeight = read_u16(f)
topSideBearing = read_i16(f)
return VerticalMetricsEntry(advanceHeight, topSideBearing)
@dataclass
class VmtxTable(Table):
vMetrics: List[VerticalMetricsEntry]
topSideBearing: List[int]
def parse_vmtx_table(f: BinaryIO, numOfLongVerMetrics: int, numGlyphs: int) -> VmtxTable:
vMetrics = [parse_vertical_metrics_entry(f) for _ in range(numOfLongVerMetrics)]
topSideBearing = [read_i16(f) for _ in range(numGlyphs-numOfLongVerMetrics)]
return VmtxTable(vMetrics, topSideBearing)
@dataclass
class BaseGlyphRecord:
pass
def parse_base_glyph_record(f: BinaryIO) -> BaseGlyphRecord:
assert False
@dataclass
class LayerRecord:
pass
def parse_layer_record(f: BinaryIO) -> LayerRecord:
assert False
@dataclass
class COLRTable(Table, ABD):
version: int
numBaseGlyphRecords: int
baseGlyphRecord: List[BaseGlyphRecord]
layerRecords: List[LayerRecord]
numLayerRecords: int
@dataclass
class COLRTable_Ver_0(COLRTable): pass
@dataclass
class BaseGlyphPaintRecord:
pass
def parse_base_glyph_paint_record(f: BinaryIO) -> BaseGlyphPaintRecord:
assert False
@dataclass
class BaseGlyphListTable(Table):
numBaseGlyphPaintRecords: int
baseGlyphPaintRecords: List[BaseGlyphPaintRecord]
def parse_base_glyph_list_table(f: BinaryIO) -> BaseGlyphListTable:
assert False
@dataclass
class LayerListTable(Table):
pass
def parse_layer_list_table(f: BinaryIO) -> LayerListTable:
assert False
@dataclass
class ClipListTable(Table):
pass
def parse_clip_list_table(f: BinaryIO) -> ClipListTable:
assert False
@dataclass
class COLRTable_Ver_1(COLRTable_Ver_0):
baseGlyphList: BaseGlyphListTable
layerList: Optional[LayerListTable]
clipList: Optional[ClipListTable]
varIndexMap: Optional['DeltaSetIndexMapTable']
itemVariationStore: Optional['ItemVariationStoreTable']
def parse_COLR_table(f: BinaryIO) -> COLRTable:
version = read_u16(f)
assert version in [0, 1], f"Invalid version: {version}"
return COLRTable_Ver_0(version, 0, [], [], 0)
assert False, f"Unimplemented: version: {version}"
@dataclass
class CPALTable(Table):
pass
def parse_CPAL_table(f: BinaryIO) -> CPALTable:
assert False
# 6.2
def is_CCXX(CC: str, tag: str) -> bool:
assert len(CC) == 2
return len(tag) == 4 and tag[:2] == CC and tag[2:].isdigit()
class CCXXTag(ABE):
__CC__ = ''
__range__ = (1, 99) # default range is CC01 - CC99
def __init__(self, tag: str):
assert len(self.__CC__) == 2, f"Subclass of CCXXTag must defined a __CC__ of length two"
if not is_CCXX(self.__CC__, tag): raise ValueError
self.num = int(tag[2:])
if not (self.__range__[0] <= self.num <= self.__range__[1]): raise ValueError(f"Invalid {self.__class__.__name__}: {self.num}. Expected number between {self.__range__[0]} and {self.__range__[1]}.")
2024-09-15 16:10:41 +10:00
# @classmethod
# def from_num(cls, num: int) -> Self:
# assert 0 <= num <= 99, f"Invalid num: {num}. Must be two digits"
# return cls(f"{cls.__CC__}{num:0>2}") # don't need to check the range because the __init__ will check
2024-05-03 21:02:58 +10:00
def __str__(self) -> str:
return f"'{self.__CC__}{self.num:0>2}'"
2024-09-15 16:10:41 +10:00
@property
def name(self) -> str:
return str(self)
2024-05-03 21:02:58 +10:00
class ScriptTag(Enum):
Adlam = 'adlm'
Ahom = 'ahom'
AnatolianHieroglyphs = 'hluw'
Arabic = 'arab'
Armenian = 'armn'
Avestan = 'avst'
Balinese = 'bali'
Bamum = 'bamu'
BassaVah = 'bass'
Batak = 'batk'
Bengali = 'beng'
Bengali_v2 = 'bng2'
Bhaiksuki = 'bhks'
Bopomofo = 'bopo'
Brahmi = 'brah'
Braille = 'brai'
Buginese = 'bugi'
Buhid = 'buhd'
ByzantineMusic = 'byzm'
CanadianSyllabics = 'cans'
Carian = 'cari'
CaucasianAlbanian = 'aghb'
Chakma = 'cakm'
Cham = 'cham'
Cherokee = 'cher'
CJKIdeographic = 'hani'
Coptic = 'copt'
CypriotSyllabary = 'cprt'
Cyrillic = 'cyrl'
Default = 'DFLT'
Deseret = 'dsrt'
Devanagari = 'deva'
Devanagari_v2 = 'dev2'
Dogra = 'dogr'
Duployan = 'dupl'
EgyptianHieroglyphs = 'egyp'
Elbasan = 'elba'
Ethiopic = 'ethi'
Georgian = 'geor'
Glagolitic = 'glag'
Gothic = 'goth'
Grantha = 'gran'
Greek = 'grek'
Gujarati = 'gujr'
Gujarati_v2 = 'gjr2'
GunjalaGondi = 'gong'
Gurmukhi = 'guru'
Gurmukhi_v2 = 'gur2'
Hangul = 'hang'
HangulJamo = 'jamo'
HanifiRohingya = 'rohg'
Hanunoo = 'hano'
Hatran = 'hatr'
Hebrew = 'hebr'
Hiragana = 'kana'
ImperialAramaic = 'armi'
InscriptionalPahlavi = 'phli'
InscriptionalParthian = 'prti'
Javanese = 'java'
Kaithi = 'kthi'
Kannada = 'knda'
Kannada_v2 = 'knd2'
Katakana = 'kana'
KayahLi = 'kali'
Kharosthi = 'khar'
Khmer = 'khmr'
Khojki = 'khoj'
Khudawadi = 'sind'
Lao = 'lao '
Latin = 'latn'
Lepcha = 'lepc'
Limbu = 'limb'
LinearA = 'lina'
LinearB = 'linb'
Lisu_Fraser = 'lisu'
Lycian = 'lyci'
Lydian = 'lydi'
Mahajani = 'mahj'
Makasar = 'maka'
Malayalam = 'mlym'
Malayalam_v2 = 'mlm2'
Mandaic_Mandaean = 'mand'
Manichaean = 'mani'
MasaramGondi = 'gonm'
Marchen = 'marc'
MathematicalAlphanumericSymbols = 'math'
Medefaidrin_OberiOkaime_OberiƆkaimɛ = 'medf' # TODO: Maybe remove non-ascii characters?
MeiteiMayek_Meithei_Meetei = 'mtei'
MendeKikakui = 'mend'
MeroiticCursive = 'merc'
MeroiticHieroglyphs = 'mero'
Miao = 'plrd'
Modi = 'modi'
Mongolian = 'mong'
Mro = 'mroo'
Multani = 'mult'
MusicalSymbols = 'musc'
Myanmar = 'mymr'
Myanmar_v2 = 'mym2'
Nabataean = 'nbat'
Newa = 'newa'
NewTaiLue = 'talu'
NKo = 'nko '
Nüshu = 'nshu'
Odia_formerlyOriya = 'orya'
Odia_formerlyOriya_v2 = 'ory2'
Ogham = 'ogam'
OlChiki = 'olck'
OldItalic = 'ital'
OldHungarian = 'hung'
OldNorthArabian = 'narb'
OldPermic = 'perm'
OldPersianCuneiform = 'xpeo'
OldSogdian = 'sogo'
OldSouthArabian = 'sarb'
OldTurkic_OrkhonRunic = 'orkh'
Osage = 'osge'
Osmanya = 'osma'
PahawhHmong = 'hmng'
Palmyrene = 'palm'
PauCinHau = 'pauc'
PhagsPa = 'phag'
Phoenician = 'phnx'
PsalterPahlavi = 'phlp'
Rejang = 'rjng'
Runic = 'runr'
Samaritan = 'samr'
Saurashtra = 'saur'
Sharada = 'shrd'
Shavian = 'shaw'
Siddham = 'sidd'
SignWriting = 'sgnw'
Sinhala = 'sinh'
Sogdian = 'sogd'
SoraSompeng = 'sora'
Soyombo = 'soyo'
SumeroAkkadianCuneiform = 'xsux'
Sundanese = 'sund'
SylotiNagri = 'sylo'
Syriac = 'syrc'
Tagalog = 'tglg'
Tagbanwa = 'tagb'
TaiLe = 'tale'
TaiTham_Lanna = 'lana'
TaiViet = 'tavt'
Takri = 'takr'
Tamil = 'taml'
Tamil_v2 = 'tml2'
Tangut = 'tang'
Telugu = 'telu'
Telugu_v2 = 'tel2'
Thaana = 'thaa'
Thai = 'thai'
Tibetan = 'tibt'
Tifinagh = 'tfng'
Tirhuta = 'tirh'
UgariticCuneiform = 'ugar'
Vai = 'vai '
WarangCiti = 'wara'
Yi = 'yi\x20\x20'
ZanabazarSquare_ZanabazarinDörböljinUseg_XewteeDörböljinBicig_HorizontalSquareScript = 'zanb' # TODO: Maybe remove non-ascii characters?
def __str__(self) -> str: return self._name_
@dataclass
class LangSysTable(Table):
lookupOrder: Optional[Table] # documentation doesn't define the table for this
requiredFeatureIndex: int
featureIndexCount: int
featuresIndices: List[int]
def parse_lang_sys_table(f: BinaryIO) -> LangSysTable:
lookupOrderOffset = read_u16(f)
assert lookupOrderOffset == 0, "Reserved"
lookupOrder = None
requiredFeatureIndex = read_u16(f)
featureIndexCount = read_u16(f)
featureIndices = [read_u16(f) for _ in range(featureIndexCount)]
return LangSysTable(lookupOrder, requiredFeatureIndex, featureIndexCount, featureIndices)
class LangSysTag(ABE): pass
class ValidLangSysTag(LangSysTag, Enum):
# Comments are the ISO 639 Codes
Abaza = 'ABA ' # abq
Abkhazian = 'ABK ' # abk
Acholi = 'ACH ' # ach
Achi = 'ACR ' # acr
Adyghe = 'ADY ' # ady
Afrikaans = 'AFK ' # afr
Afar = 'AFR ' # aar
Agaw = 'AGW ' # ahg
Aiton = 'AIO ' # aio
Akan = 'AKA ' # aka
Alsatian = 'ALS ' # gsw
Altai = 'ALT ' # atv, alt
Amharic = 'AMH ' # amh
AngloSaxon = 'ANG ' # ang
PhoneticTranscription_AmericanistConventions = 'APPH'
Arabic = 'ARA ' # ara
Aragonese = 'ARG ' # arg
Aari = 'ARI ' # aiw
Rakhine = 'ARK ' # mhv, rmz, rki
Assamese = 'ASM ' # asm
Asturian = 'AST ' # ast
Athapaskan = 'ATH ' # apk, apj, apl, apm, apw, nav, bea, sek, bcr, caf, crx, clc, gwi, haa, chp, dgr, scs, xsl, srs, ing, hoi, koy, hup, ktw, mvb, wlk, coq, ctc, gce, tol, tuu, kkz, tgx, tht, aht, tfn, taa, tau, tcb, kuu, tce, ttm, txc
Avar = 'AVR ' # ava
Awadhi = 'AWA ' # awa
Aymara = 'AYM ' # aym
Torki = 'AZB ' # azb
Azerbaijani = 'AZE ' # aze
Badaga = 'BAD ' # bfq
Banda = 'BAD0' # bad
Baghelkhandi = 'BAG ' # bfy
Balkar = 'BAL ' # krc
Balinese = 'BAN ' # ban
Bavarian = 'BAR ' # bar
Baoulé = 'BAU ' # bci
BatakToba = 'BBC ' # bbc
Berber = 'BBR '
Bench = 'BCH ' # bcq
BibleCree = 'BCR '
Bandjalang = 'BDY ' # bdy
Belarusian = 'BEL ' # bel
Bemba = 'BEM ' # bem
Bengali = 'BEN ' # ben
Haryanvi = 'BGC ' # bgc
Bagri = 'BGQ ' # bgq
Bulgarian = 'BGR ' # bul
Bhili = 'BHI ' # bhi, bhb
Bhojpuri = 'BHO ' # bho
Bikol = 'BIK ' # bik, bhk, bcl, bto, cts, bln
Bilen = 'BIL ' # byn
Bislama = 'BIS ' # bis
Kanauji = 'BJJ ' # bjj
Blackfoot = 'BKF ' # bla
Baluchi = 'BLI ' # bal
PaoKaren = 'BLK ' # blk
Balante = 'BLN ' # bjt, ble
Balti = 'BLT ' # bft
Bambara_Bamanankan = 'BMB ' # bam
Bamileke = 'BML '
Bosnian = 'BOS ' # bos
BishnupriyaManipuri = 'BPY ' # bpy
Breton = 'BRE ' # bre
Brahui = 'BRH ' # brh
BrajBhasha = 'BRI ' # bra
Burmese = 'BRM ' # mya
Bodo = 'BRX ' # brx
Bashkir = 'BSH ' # bak
Burushaski = 'BSK ' # bsk
Beti = 'BTI ' # btb
BatakSimalungun = 'BTS ' # bts
Bugis = 'BUG ' # bug
Medumba = 'BYV ' # byv
Kaqchikel = 'CAK ' # cak
Catalan = 'CAT ' # cat
ZamboangaChavacano = 'CBK ' # cbk
Chinantec = 'CCHN' # cco, chj, chq, chz, cle, cnl, cnt, cpa, csa, cso, cte, ctl, cuc, cvn
Cebuano = 'CEB ' # ceb
Chechen = 'CHE ' # che
ChahaGurage = 'CHG ' # sgw
Chattisgarhi = 'CHH ' # hne
Chichewa_Chewa_Nyanja = 'CHI ' # nya
Chukchi = 'CHK ' # ckt
Chuukese = 'CHK0' # chk
Choctaw = 'CHO ' # cho
Chipewyan = 'CHP ' # chp
Cherokee = 'CHR ' # chr
Chamorro = 'CHA ' # cha
Chuvash = 'CHU ' # chv
Cheyenne = 'CHY ' # chy
Chiga = 'CGG ' # cgg
WesternCham = 'CJA ' # cja
EasternCham = 'CJM ' # cjm
Comorian = 'CMR ' # swb, wlc, wni, zdj
Coptic = 'COP ' # cop
Cornish = 'COR ' # cor
Corsican = 'COS ' # cos
Creoles = 'CPP ' # cpp
Cree = 'CRE ' # cre
Carrier = 'CRR ' # crx, caf
CrimeanTatar = 'CRT ' # crh
Kashubian = 'CSB ' # csb
ChurchSlavonic = 'CSL ' # chu
Czech = 'CSY ' # ces
Chittagonian = 'CTG ' # ctg
SanBlasKuna = 'CUK ' # cuk
Danish = 'DAN ' # dan
Dargwa = 'DAR ' # dar
Dayi = 'DAX ' # dax
WoodsCree = 'DCR ' # cwd
German = 'DEU ' # deu
Dogri = 'DGO ' # dgo
Dogri_ = 'DGR ' # doi
Dhangu = 'DHG ' # dhg
Divehi_Dhivehi_Maldivian_ = 'DHV ' # (deprecated) div
Dimli = 'DIQ ' # diq
Divehi_Dhivehi_Maldivian = 'DIV ' # div
Zarma = 'DJR ' # dje
Djambarrpuyngu = 'DJR0' # djr
Dangme = 'DNG ' # ada
Dan = 'DNJ ' # dnj
Dinka = 'DNK ' # din
Dari = 'DRI ' # prs
Dhuwal = 'DUJ ' # duj
Dungan = 'DUN ' # dng
Dzongkha = 'DZN ' # dzo
Ebira = 'EBI ' # igb
EasternCree = 'ECR ' # crj, crl
Edo = 'EDO ' # bin
Efik = 'EFI ' # efi
Greek = 'ELL ' # ell
EasternManinkakan = 'EMK ' # emk
English = 'ENG ' # eng
Erzya = 'ERZ ' # myv
Spanish = 'ESP ' # spa
CentralYupik = 'ESU ' # esu
Estonian = 'ETI ' # est
Basque = 'EUQ ' # eus
Evenki = 'EVK ' # evn
Even = 'EVN ' # eve
Ewe = 'EWE ' # ewe
FrenchAntillean = 'FAN ' # acf
Fang = 'FAN0' # fan
Persian = 'FAR ' # fas
Fanti = 'FAT ' # fat
Finnish = 'FIN ' # fin
Fijian = 'FJI ' # fij
Dutch_Flemish = 'FLE ' # vls
Fefe = 'FMP ' # fmp
ForestNenets = 'FNE ' # enf
Fon = 'FON ' # fon
Faroese = 'FOS ' # fao
French = 'FRA ' # fra
CajunFrench = 'FRC ' # frc
Frisian = 'FRI ' # fry
Friulian = 'FRL ' # fur
Arpitan = 'FRP ' # frp
Futa = 'FTA ' # fuf
Fulah = 'FUL ' # ful
NigerianFulfulde = 'FUV ' # fuv
Ga = 'GAD ' # gaa
ScottishGaelic_Gaelic = 'GAE ' # gla
Gagauz = 'GAG ' # gag
Galician = 'GAL ' # glg
Garshuni = 'GAR '
Garhwali = 'GAW ' # gbm
Geez = 'GEZ ' # gez
Githabul = 'GIH ' # gih
Gilyak = 'GIL ' # niv
Kiribati_Gilbertese = 'GIL0' # gil
Kpelle_Guinea = 'GKP ' # gkp
Gilaki = 'GLK ' # glk
Gumuz = 'GMZ ' # guk
Gumatj = 'GNN ' # gnn
Gogo = 'GOG ' # gog
Gondi = 'GON ' # gon, gno, ggo
Greenlandic = 'GRN ' # kal
Garo = 'GRO ' # grt
Guarani = 'GUA ' # grn
Wayuu = 'GUC ' # guc
Gupapuyngu = 'GUF ' # guf
Gujarati = 'GUJ ' # guj
Gusii = 'GUZ ' # guz
Haitian_HaitianCreole = 'HAI ' # hat
Halam = 'HAL ' # flm
Harauti = 'HAR ' # hoj
Hausa = 'HAU ' # hau
Hawaiian = 'HAW ' # haw
Haya = 'HAY ' # hay
Hazaragi = 'HAZ ' # haz
HammerBanna = 'HBN ' # amf
Herero = 'HER ' # her
Hiligaynon = 'HIL ' # hil
Hindi = 'HIN ' # hin
HighMari = 'HMA ' # mrj
Hmong = 'HMN ' # hmn
HiriMotu = 'HMO ' # hmo
Hindko = 'HND ' # hno, hnd
Ho = 'HO ' # hoc
Harari = 'HRI ' # har
Croatian = 'HRV ' # hrv
Hungarian = 'HUN ' # hun
Armenian = 'HYE ' # hye
ArmenianEast = 'HYE0' # hye
Iban = 'IBA ' # iba
Ibibio = 'IBB ' # ibb
Igbo = 'IBO ' # ibo
IjoLanguages = 'IJO ' # ijc
Ido = 'IDO ' # ido
Interlingue = 'ILE ' # ile
Ilokano = 'ILO ' # ilo
Interlingua = 'INA ' # ina
Indonesian = 'IND ' # ind
Ingush = 'ING ' # inh
Inuktitut = 'INU ' # iku
Inupiat = 'IPK ' # ipk
PhoneticTranscription_IPAConventions = 'IPPH'
Irish = 'IRI ' # gle
IrishTraditional = 'IRT ' # gle
Icelandic = 'ISL ' # isl
InariSami = 'ISM ' # smn
Italian = 'ITA ' # ita
Hebrew = 'IWR ' # heb
Javanese = 'JAV ' # jav
Yiddish = 'JII ' # yid
JamaicanCreole = 'JAM ' # jam
Japanese = 'JAN ' # jpn
Lojban = 'JBO ' # jbo
Krymchak = 'JCT ' # jct
Ladino = 'JUD ' # lad
Jula = 'JUL ' # dyu
Kabardian = 'KAB ' # kbd
Kabyle = 'KAB0' # kab
Kachchi = 'KAC ' # kfr
Kalenjin = 'KAL ' # kln
Kannada = 'KAN ' # kan
Karachay = 'KAR ' # krc
Georgian = 'KAT ' # kat
Kazakh = 'KAZ ' # kaz
Makonde = 'KDE ' # kde
KabuverdianuCrioulo = 'KEA ' # kea
Kebena = 'KEB ' # ktb
Kekchi = 'KEK ' # kek
KhutsuriGeorgian = 'KGE ' # kat
Khakass = 'KHA ' # kjh
KhantyKazim = 'KHK ' # kca
Khmer = 'KHM ' # khm
KhantyShurishkar = 'KHS ' # kca
KhamtiShan = 'KHT ' # kht
KhantyVakhi = 'KHV ' # kca
Khowar = 'KHW ' # khw
Kikuyu_Gikuyu = 'KIK ' # kik
Kirghiz_Kyrgyz = 'KIR ' # kir
Kisii = 'KIS ' # kqs, kss
Kirmanjki = 'KIU ' # kiu
SouthernKiwai = 'KJD ' # kjd
EasternPwoKaren = 'KJP ' # kjp
Kokni = 'KKN ' # kex
Kalmyk = 'KLM ' # xal
Kamba = 'KMB ' # kam
Kumaoni = 'KMN ' # kfy
Komo = 'KMO ' # kmw
Komso = 'KMS ' # kxc
KhorasaniTurkic = 'KMZ ' # kmz
Kanuri = 'KNR ' # kau
Kodagu = 'KOD ' # kfa
KoreanOldHangul = 'KOH ' # okm
Konkani = 'KOK ' # kok
Kikongo = 'KON ' # ktu
Kongo = 'KON0' # kon
Komi = 'KOM ' # kom
KomiPermyak = 'KOP ' # koi
Korean = 'KOR ' # kor
Kosraean = 'KOS ' # kos
KomiZyrian = 'KOZ ' # kpv
Kpelle = 'KPL ' # kpe
Krio = 'KRI ' # kri
Karakalpak = 'KRK ' # kaa
Karelian = 'KRL ' # krl
Karaim = 'KRM ' # kdr
Karen = 'KRN ' # kar
Koorete = 'KRT ' # kqy
Kashmiri = 'KSH ' # kas
Ripuarian = 'KSH0' # ksh
Khasi = 'KSI ' # kha
KildinSami = 'KSM ' # sjd
SgawKaren = 'KSW ' # ksw
Kuanyama = 'KUA ' # kua
Kui = 'KUI ' # kxu
Kulvi = 'KUL ' # kfx
Kumyk = 'KUM ' # kum
Kurdish = 'KUR ' # kur
Kurukh = 'KUU ' # kru
Kuy = 'KUY ' # kdt
Koryak = 'KYK ' # kpy
WesternKayah = 'KYU ' # kyu
Ladin = 'LAD ' # lld
Lahuli = 'LAH ' # bfu
Lak = 'LAK ' # lbe
Lambani = 'LAM ' # lmn
Lao = 'LAO ' # lao
Latin = 'LAT ' # lat
Laz = 'LAZ ' # lzz
LCree = 'LCR ' # crm
Ladakhi = 'LDK ' # lbj
Lezgi = 'LEZ ' # lez
Ligurian = 'LIJ ' # lij
Limburgish = 'LIM ' # lim
Lingala = 'LIN ' # lin
Lisu = 'LIS ' # lis
Lampung = 'LJP ' # ljp
Laki = 'LKI ' # lki
LowMari = 'LMA ' # mhr
Limbu = 'LMB ' # lif
Lombard = 'LMO ' # lmo
Lomwe = 'LMW ' # ngl
Loma = 'LOM ' # lom
Luri = 'LRC ' # lrc, luz, bqi, zum
LowerSorbian = 'LSB ' # dsb
LuleSami = 'LSM ' # smj
Lithuanian = 'LTH ' # lit
Luxembourgish = 'LTZ ' # ltz
LubaLulua = 'LUA ' # lua
LubaKatanga = 'LUB ' # lub
Ganda = 'LUG ' # lug
Luyia = 'LUH ' # luy
Luo = 'LUO ' # luo
Latvian = 'LVI ' # lav
Madura = 'MAD ' # mad
Magahi = 'MAG ' # mag
Marshallese = 'MAH ' # mah
Majang = 'MAJ ' # mpe
Makhuwa = 'MAK ' # vmw
MalayalamTraditional = 'MAL ' # mal
Mam = 'MAM ' # mam
Mansi = 'MAN ' # mns
Mapudungun = 'MAP ' # arn
Marathi = 'MAR ' # mar
Marwari = 'MAW ' # mwr, dhd, rwr, mve, wry, mtr, swv
Mbundu = 'MBN ' # kmb
Mbo = 'MBO ' # mbo
Manchu = 'MCH ' # mnc
MooseCree = 'MCR ' # crm
Mende = 'MDE ' # men
Mandar = 'MDR ' # mdr
Meen = 'MEN ' # mym
Meru = 'MER ' # mer
Morisyen = 'MFE ' # mfe
Minangkabau = 'MIN ' # min
Mizo = 'MIZ ' # lus
Macedonian = 'MKD ' # mkd
Makasar = 'MKR ' # mak
Kituba = 'MKW ' # mkw
Male = 'MLE ' # mdy
Malagasy = 'MLG ' # mlg
Malinke = 'MLN ' # mlq
MalayalamReformed = 'MLR ' # mal
Malay = 'MLY ' # msa
Mandinka = 'MND ' # mnk
Mongolian = 'MNG ' # mon
Manipuri = 'MNI ' # mni
Maninka = 'MNK ' # man, mnk, myq, mku, msc, emk, mwk, mlq
Manx = 'MNX ' # glv
Mohawk = 'MOH ' # mho
Moksha = 'MOK ' # mdf
Moldavian = 'MOL ' # mol
Mon = 'MON ' # mnw
Moroccan = 'MOR '
Mossi = 'MOS ' # mos
Maori = 'MRI ' # mri
Maithili = 'MTH ' # mai
Maltese = 'MTS ' # mlt
Mundari = 'MUN ' # unr
Muscogee = 'MUS ' # mus
Mirandese = 'MWL ' # mwl
HmongDaw = 'MWW ' # mww
Mayan = 'MYN ' # myn
Mazanderani = 'MZN ' # mzn
NagaAssamese = 'NAG ' # nag
Nahuatl = 'NAH ' # nah
Nanai = 'NAN ' # gld
Neapolitan = 'NAP ' # nap
Naskapi = 'NAS ' # nsk
Nauruan = 'NAU ' # nau
Navajo = 'NAV ' # nav
NCree = 'NCR ' # csw
Ndebele = 'NDB ' # nbl, nde
Ndau = 'NDC ' # ndc
Ndonga = 'NDG ' # ndo
LowSaxon = 'NDS ' # nds
Nepali = 'NEP ' # nep
Newari = 'NEW ' # new
Ngbaka = 'NGA ' # nga
Nagari = 'NGR '
NorwayHouseCree = 'NHC ' # csw
Nisi = 'NIS ' # dap
Niuean = 'NIU ' # niu
Nyankole = 'NKL ' # nyn
NKo = 'NKO ' # ngo
Dutch = 'NLD ' # nld
Nimadi = 'NOE ' # noe
Nogai = 'NOG ' # nog
Norwegian = 'NOR ' # nob
Novial = 'NOV ' # nov
NorthernSami = 'NSM ' # sme
Sotho_Northern = 'NSO ' # nso
NorthernThai = 'NTA ' # nod
Esperanto = 'NTO ' # epo
Nyamwezi = 'NYM ' # nym
NorwegianNynorsk_Nynorsk_Norwegian = 'NYN ' # nno
MbembeTigon = 'NZA ' # nza
Occitan = 'OCI ' # oci
OjiCree = 'OCR ' # ojs
Ojibway = 'OJB ' # oji
Odia_formerlyOriya = 'ORI ' # ori
Oromo = 'ORO ' # orm
Ossetian = 'OSS ' # oss
PalestinianAramaic = 'PAA ' # sam
Pangasinan = 'PAG ' # pag
Pali = 'PAL ' # pli
Pampangan = 'PAM ' # pam
Punjabi = 'PAN ' # pan
Palpa = 'PAP ' # plp
Papiamentu = 'PAP0' # pap
Pashto = 'PAS ' # pus
Palauan = 'PAU ' # pau
Bouyei = 'PCC ' # pcc
Picard = 'PCD ' # pcd
PennsylvaniaGerman = 'PDC ' # pdc
PolytonicGreek = 'PGR ' # ell
Phake = 'PHK ' # phk
Norfolk = 'PIH ' # pih
Filipino = 'PIL ' # fil
Palaung = 'PLG ' # pce, rbb, pll
Polish = 'PLK ' # pol
Piemontese = 'PMS ' # pms
WesternPanjabi = 'PNB ' # pnb
Pocomchi = 'POH ' # poh
Pohnpeian = 'PON ' # pon
Provencal = 'PRO ' # pro
Portuguese = 'PTG ' # por
WesternPwoKaren = 'PWO ' # pwo
Chin = 'QIN ' # bgr, cnh, cnw, czt, sez, tcp, csy, ctd, flm, pck, tcz, zom, cmr, dao, hlt, cka, cnk, mrh, cbl, cnb, csh
Kiche = 'QUC ' # quc
Quechua_Bolivia = 'QUH ' # quh
Quechua = 'QUZ ' # quz
Quechua_Ecuador = 'QVI ' # qvi
Quechua_Peru = 'QWH ' # qwh
Rajasthani = 'RAJ ' # raj
Rarotongan = 'RAR ' # rar
RCree = 'RCR ' # atj
RussianBuriat = 'RBU ' # bxr
Rejang = 'REJ ' # rej
Riang = 'RIA ' # ria
Tarifit = 'RIF ' # rif
Ritarungo = 'RIT ' # rit
Arakwal = 'RKW ' # rkw
Romansh = 'RMS ' # roh
VlaxRomani = 'RMY ' # rmy
Romanian = 'ROM ' # ron
Romany = 'ROY ' # rom
Rusyn = 'RSY ' # rue
Rotuman = 'RTM ' # rtm
Kinyarwanda = 'RUA ' # kin
Rundi = 'RUN ' # run
Aromanian = 'RUP ' # rup
Russian = 'RUS ' # rus
Sadri = 'SAD ' # sck
Sanskrit = 'SAN ' # san
Sasak = 'SAS ' # sas
Santali = 'SAT ' # sat
Sayisi = 'SAY ' # chp
Sicilian = 'SCN ' # scn
Scots = 'SCO ' # sco
NorthSlavey = 'SCS ' # scs
Sekota = 'SEK ' # xan
Selkup = 'SEL ' # sel
OldIrish = 'SGA ' # sga
Sango = 'SGO ' # sag
Samogitian = 'SGS ' # sgs
Tachelhit = 'SHI ' # shi
Shan = 'SHN ' # shn
Sibe = 'SIB ' # sjo
Sidamo = 'SID ' # sid
SilteGurage = 'SIG ' # xst
SkoltSami = 'SKS ' # sms
Slovak = 'SKY ' # slk
Slavey = 'SLA ' # scs, xsl
Slovenian = 'SLV ' # slv
Somali = 'SML ' # som
Samoan = 'SMO ' # smo
Sena = 'SNA ' # she
Shona = 'SNA0' # sna
Sindhi = 'SND ' # snd
Sinhala_Sinhalese = 'SNH ' # sin
Soninke = 'SNK ' # snk
SodoGurage = 'SOG ' # gru
Songe = 'SOP ' # sop
Sotho_Southern = 'SOT ' # sot
Albanian = 'SQI ' # gsw
Serbian = 'SRB ' # srp
Sardinian = 'SRD ' # srd
Seraiki = 'SRK ' # skr
Serer = 'SRR ' # srr
SouthSlavey = 'SSL ' # xsl
SouthernSami = 'SSM ' # sma
SaterlandFrisian = 'STQ ' # stq
Sukuma = 'SUK ' # suk
Sundanese = 'SUN ' # sun
Suri = 'SUR ' # suq
Svan = 'SVA ' # sva
Swedish = 'SVE ' # swe
SwadayaAramaic = 'SWA ' # aii
Swahili = 'SWK ' # swa
Swati = 'SWZ ' # ssw
Sutu = 'SXT ' # ngo
UpperSaxon = 'SXU ' # sxu
Sylheti = 'SYL ' # syl
Syriac = 'SYR ' # aii, amw, cld, syc, syr, tru
Syriac_EstrangelaScriptVariant = 'SYRE' # syc, syr # equivalent to ISO 15924 'Syre'
Syriac_WesternScriptVariant = 'SYRJ' # syc, syr # equivalent to ISO 15924 'Syrj'
Syriac_EasternScriptVariant = 'SYRN' # syc, syr # equivalent to ISO 15924 'Syrn'
Silesian = 'SZL ' # szl
Tabasaran = 'TAB ' # tab
Tajik = 'TAJ ' # tgk
Tamil = 'TAM ' # tam
Tatar = 'TAT ' # tat
THCree = 'TCR ' # cwd
DehongDai = 'TDD ' # tdd
Telugu = 'TEL ' # tel
Tetum = 'TET ' # tet
Tagalog = 'TGL ' # tgl
Tongan = 'TGN ' # ton
Tigre = 'TGR ' # tig
Tigrinya = 'TGY ' # tir
Thai = 'THA ' # tha
Tahitian = 'THT ' # tah
Tibetan = 'TIB ' # bod
Tiv = 'TIV ' # tiv
Turkmen = 'TKM ' # tuk
Tamashek = 'TMH ' # tmh
Temne = 'TMN ' # tem
Tswana = 'TNA ' # tsn
TundraNenets = 'TNE ' # enh
Tonga = 'TNG ' # toi
Todo = 'TOD ' # xal
Toma = 'TOD0' # tod
TokPisin = 'TPI ' # tpi
Turkish = 'TRK ' # tur
Tsonga = 'TSG ' # tso
TuroyoAramaic = 'TUA ' # tru
Tulu = 'TUL ' # tcy
Tumbuka = 'TUM ' # tum
Tuvin = 'TUV ' # tyv
Tuvalu = 'TVL ' # tvl
Twi = 'TWI ' # twi
Tày = 'TYZ ' # tyz
Tamazight = 'TZM ' # tzm
Tzotzil = 'TZO ' # tzo
Udmurt = 'UDM ' # udm
Ukrainian = 'UKR ' # ukr
Umbundu = 'UMB ' # umb
Urdu = 'URD ' # urd
UpperSorbian = 'USB ' # hsb
Uyghur = 'UYG ' # uig
Uzbek = 'UZB ' # uzb, uzn, uzs
Venetian = 'VEC ' # vec
Venda = 'VEN ' # ven
Vietnamese = 'VIT ' # vie
Volapük = 'VOL ' # vol # TODO: Maybe remove non-ascii characters? (cute fraud)
Võro = 'VRO ' # vro # TODO: Maybe remove non-ascii characters?
Wa = 'WA ' # wbm
Wagdi = 'WAG ' # wbr
WarayWaray = 'WAR ' # war
WestCree = 'WCR ' # crk
Welsh = 'WEL ' # cym
Walloon = 'WLN ' # wln
Wolof = 'WLF ' # wol
Mewati = 'WTM ' # wtm
= 'XBD ' # khb
Xhosa = 'XHS ' # xho
Minjangbal = 'XJB ' # xjb
Soga = 'XOG ' # xog
Kpelle_Liberia = 'XPE ' # xpe
Sakha = 'YAK ' # sah
Yao = 'YAO ' # yao
Yapese = 'YAP ' # yap
Yoruba = 'YBA ' # yor
YCree = 'YCR ' # cre
YiClassic = 'YIC '
YiModern = 'YIM ' # iii
Zealandic = 'ZEA ' # zea
StandardMorrocanTamazigh = 'ZGH ' # zgh
Zhuang = 'ZHA ' # zha
Chinese_HongKongSAR = 'ZHH ' # zho
ChinesePhonetic = 'ZHP ' # zho
ChineseSimplified = 'ZHS ' # zho
ChineseTraditional = 'ZHT ' # zho
Zande = 'ZND ' # zne
Zulu = 'ZUL ' # zul
Zazaki = 'ZZA ' # zza
def __str__(self) -> str: return self._name_
2024-09-15 16:10:41 +10:00
@dataclass
class InvalidLangSysTag(LangSysTag):
tag: str
2024-05-03 21:02:58 +10:00
def parse_lang_sys_tag(f: BinaryIO) -> LangSysTag:
2024-09-15 16:10:41 +10:00
return read_tag_from_tags(f, ValidLangSysTag, InvalidLangSysTag, MS_VOLT_Tag, umbrellaTagCls=LangSysTag)
2024-05-03 21:02:58 +10:00
@dataclass
class LangSysRecord:
langSysTag: LangSysTag
langSys: LangSysTable
def parse_lang_sys_record(f: BinaryIO, start_tell:int) -> LangSysRecord:
langSysTag = parse_lang_sys_tag(f)
langSysOffset = read_u16(f)
with SaveTell(f):
langSys = parse_at_offset(f, start_tell, langSysOffset, parse_lang_sys_table)
return LangSysRecord(langSysTag, langSys)
@dataclass
class ScriptTable(Table):
defaultLangSys: Optional[LangSysTable]
langSysCount: int
langSysRecords: List[LangSysRecord]
def parse_script_table(f: BinaryIO) -> ScriptTable:
start_tell = f.tell()
defaultLangSysOffset = read_u16(f)
with SaveTell(f):
defaultLangSys = parse_at_optional_offset(f, start_tell, defaultLangSysOffset, parse_lang_sys_table)
langSysCount = read_u16(f)
langSysRecords = [parse_lang_sys_record(f, start_tell) for _ in range(langSysCount)]
return ScriptTable(defaultLangSys, langSysCount, langSysRecords)
@dataclass
class ScriptRecord:
scriptTag: ScriptTag
script: ScriptTable
def parse_script_record(f: BinaryIO, start_tell:int) -> ScriptRecord:
2024-05-04 11:34:37 +10:00
tag = read_tag(f, ScriptTag)
2024-05-03 21:02:58 +10:00
scriptOffset = read_u16(f)
with SaveTell(f):
script = parse_at_offset(f, start_tell, scriptOffset, parse_script_table)
return ScriptRecord(tag, script)
@dataclass
class ScriptListTable(Table):
scriptCount: int
scriptRecords: List[ScriptRecord]
def parse_script_list_table(f: BinaryIO) -> ScriptListTable:
start_tell = f.tell()
scriptCount = read_u16(f)
scriptRecords = [parse_script_record(f, start_tell) for _ in range(scriptCount)]
return ScriptListTable(scriptCount, scriptRecords)
class FeatureTag(ABE): pass
class SimpleFeatureTag(FeatureTag, Enum):
Aalt = 'aalt' # Access All Alternates
Abvf = 'abvf' # Above-base Forms
Abvm = 'abvm' # Above-base Mark Positioning
Abvs = 'abvs' # Above-base Substitutions
Afrc = 'afrc' # Alternative Fractions
Akhn = 'akhn' # Akhands
Blwf = 'blwf' # Below-base Forms
Blwm = 'blwm' # Below-base Mark Positioning
Blws = 'blws' # Below-base Substitutions
Calt = 'calt' # Contextual Alternates
Case = 'case' # Case-Sensitive Forms
Ccmp = 'ccmp' # Glyph Composition / Decomposition
Cfar = 'cfar' # Conjunct Form After Ro
Cjct = 'cjct' # Conjunct Forms
Clig = 'clig' # Contextual Ligatures
Cpct = 'cpct' # Centered CJK Punctuation
Cpsp = 'cpsp' # Capital Spacing
Cswh = 'cswh' # Contextual Swash
Curs = 'curs' # Cursive Positioning
# Charcater Variant 1-99 are handled by CvXXFeatureTag
C2pc = 'c2pc' # Petite Capitals From Capitals
C2sc = 'c2sc' # Small Capitals From Capitals
Dist = 'dist' # Distances
Dlig = 'dlig' # Discretionary Ligatures
Dnom = 'dnom' # Denominators
Dtls = 'dtls' # Dotless Forms
Expt = 'expt' # Expert Forms
Falt = 'falt' # Final Glyph on Line Alternates
Fin2 = 'fin2' # Terminal Forms #2
Fin3 = 'fin3' # Terminal Forms #3
Fina = 'fina' # Terminal Forms
Flac = 'flac' # Flattened ascent forms
Frac = 'frac' # Fractions
Fwid = 'fwid' # Full Widths
Half = 'half' # Half Forms
Haln = 'haln' # Halant Forms
Halt = 'halt' # Alternate Half Widths
Hist = 'hist' # Historical Forms
Hkna = 'hkna' # Horizontal Kana Alternates
Hlig = 'hlig' # Historical Ligatures
Hngl = 'hngl' # Hangul
Hojo = 'hojo' # Hojo Kanji Forms (JIS X 0212-1990 Kanji Forms)
Hwid = 'hwid' # Half Widths
Init = 'init' # Initial Forms
Isol = 'isol' # Isolated Forms
Ital = 'ital' # Italics
Jalt = 'jalt' # Justification Alternates
Jp78 = 'jp78' # JIS78 Forms
Jp83 = 'jp83' # JIS83 Forms
Jp90 = 'jp90' # JIS90 Forms
Jp04 = 'jp04' # JIS2004 Forms
Kern = 'kern' # Kerning
Lfbd = 'lfbd' # Left Bounds
Liga = 'liga' # Standard Ligatures
Ljmo = 'ljmo' # Leading Jamo Forms
Lnum = 'lnum' # Lining Figures
Locl = 'locl' # Localized Forms
Ltra = 'ltra' # Left-to-right glyph alternates
Ltrm = 'ltrm' # Left-to-right mirrored forms
Mark = 'mark' # Mark Positioning
Med2 = 'med2' # Medial Forms #2
Medi = 'medi' # Medial Forms
Mgrk = 'mgrk' # Mathematical Greek
Mkmk = 'mkmk' # Mark to Mark Positioning
Mset = 'mset' # Mark Positioning via Substitution
Nalt = 'nalt' # Alternate Annotation Forms
Nlck = 'nlck' # NLC Kanji Forms
Nukt = 'nukt' # Nukta Forms
Numr = 'numr' # Numerators
Onum = 'onum' # Oldstyle Figures
Opbd = 'opbd' # Optical Bounds
Ordn = 'ordn' # Ordinals
Ornm = 'ornm' # Ornaments
Palt = 'palt' # Proportional Alternate Widths
Pcap = 'pcap' # Petite Capitals
Pkna = 'pkna' # Proportional Kana
Pnum = 'pnum' # Proportional Figures
Pref = 'pref' # Pre-Base Forms
Pres = 'pres' # Pre-base Substitutions
Pstf = 'pstf' # Post-base Forms
Psts = 'psts' # Post-base Substitutions
Pwid = 'pwid' # Proportional Widths
Qwid = 'qwid' # Quarter Widths
Rand = 'rand' # Randomize
Rclt = 'rclt' # Required Contextual Alternates
Rkrf = 'rkrf' # Rakar Forms
Rlig = 'rlig' # Required Ligatures
Rphf = 'rphf' # Reph Forms
Rtbd = 'rtbd' # Right Bounds
Rtla = 'rtla' # Right-to-left alternates
Rtlm = 'rtlm' # Right-to-left mirrored forms
Ruby = 'ruby' # Ruby Notation Forms
Rvrn = 'rvrn' # Required Variation Alternates
Salt = 'salt' # Stylistic Alternates
Sinf = 'sinf' # Scientific Inferiors
Size = 'size' # Optical size
Smcp = 'smcp' # Small Capitals
Smpl = 'smpl' # Simplified Forms
# Stylistic Set 1-20 are handled by SsXXFeatureTag
Ssty = 'ssty' # Math script style alternates
Stch = 'stch' # Stretching Glyph Decomposition
Subs = 'subs' # Subscript
Sups = 'sups' # Superscript
Swsh = 'swsh' # Swash
Titl = 'titl' # Titling
Tjmo = 'tjmo' # Trailing Jamo Forms
Tnam = 'tnam' # Traditional Name Forms
Tnum = 'tnum' # Tabular Figures
Trad = 'trad' # Traditional Forms
Twid = 'twid' # Third Widths
Unic = 'unic' # Unicase
Valt = 'valt' # Alternate Vertical Metrics
Vatu = 'vatu' # Vattu Variants
Vert = 'vert' # Vertical Writing
Vhal = 'vhal' # Alternate Vertical Half Metrics
Vjmo = 'vjmo' # Vowel Jamo Forms
Vkna = 'vkna' # Vertical Kana Alternates
Vkrn = 'vkrn' # Vertical Kerning
Vpal = 'vpal' # Proportional Alternate Vertical Metrics
Vrt2 = 'vrt2' # Vertical Alternates and Rotation
Vrtr = 'vrtr' # Vertical Alternates for Rotation
Zero = 'zero' # Slashed Zero
def __str__(self) -> str: return self._value_
class CvXXFeatureTag(FeatureTag, CCXXTag):
"""
Character Variant XX
"""
__CC__ = 'cv'
class SsXXFeatureTag(FeatureTag, CCXXTag):
"""
Stylistic Set XX
"""
__CC__ = 'ss'
__range__ = (1, 20)
class MS_VOLT_Tag(CCXXTag, LangSysTag, FeatureTag):
"""
Tags in the form `zzXX`, produced by an error in MS VOLT.
"""
__CC__ = 'zz'
__range__ = (0, 99) # I don't know if zz00 is valid or not, but I am letting it be, so that it can be caught, because zzXX is not a valid tag for anything
2024-09-15 16:10:41 +10:00
def is_vendor_feature_tag(tag: str) -> bool:
return all(map(is_upper, tag))
@dataclass
class VendorFeatureTag(FeatureTag):
tag: str
def __post_init__(self):
if not is_vendor_feature_tag(self.tag): raise ValueError
2024-05-03 21:02:58 +10:00
def parse_feature_tag(f: BinaryIO) -> FeatureTag:
2024-09-15 16:10:41 +10:00
return read_tag_with_conditions(f, (always, SimpleFeatureTag), (always, CvXXFeatureTag), (always, SsXXFeatureTag), (is_vendor_feature_tag, VendorFeatureTag), (lambda s: is_CCXX('zz', s), MS_VOLT_Tag), umbrellaTagCls=FeatureTag)
2024-05-03 21:02:58 +10:00
@dataclass
class FeatureParamsTable(Table, ABD): pass
@dataclass
class CvXXFeatureParamsTable(FeatureParamsTable, ABD):
format: int
@dataclass
class CvXXFeatureParamsTable_Format_0(CvXXFeatureParamsTable):
featUiLabelNameId: Optional[int]
featUiTooltipTextNameId: Optional[int]
sampleTextNameId: Optional[int]
numNamedParameters: int
firstParamUiLabelNameId: Optional[int] # None means that it is 0, which means that there are no parameters
charCount: int
character: List[int]
@dataclass
class SsXXFeatureParamsTable(FeatureParamsTable, ABD):
version: int
UINameID: int
@dataclass
class SsXXFeatureParamsTable_Ver_0(SsXXFeatureParamsTable): pass
@dataclass
class SizeFeatureParamsTable(FeatureParamsTable):
design_size: int
subfamily_identifier: int
subfamily_nameID: Optional[int]
recommended_usage_range: Optional[Tuple[int, int]]
2024-05-04 11:34:37 +10:00
def parse_feature_params_table(f: BinaryIO, featureTag: FeatureTag) -> FeatureParamsTable:
2024-05-03 21:02:58 +10:00
match featureTag:
case CvXXFeatureTag(num=_):
format = read_u16(f)
assert format in [0]
match format:
case 0:
featUiLabelNameId = null_if_zero(read_u16(f))
featUiTooltipTextNameId = null_if_zero(read_u16(f))
sampleTextNameId = null_if_zero(read_u16(f))
numNamedParameters = read_u16(f)
firstParamUiLabelNameId = null_if_zero(read_u16(f))
if numNamedParameters == 0: assert firstParamUiLabelNameId is None, f"Cannot specify paramUiLabelNameIDs if numNamedParameters is 0"
charCount = read_u16(f)
character = [read_u24(f) for _ in range(charCount)]
return CvXXFeatureParamsTable_Format_0(format, featUiLabelNameId, featUiTooltipTextNameId, sampleTextNameId, numNamedParameters, firstParamUiLabelNameId, charCount, character)
case _:
assert False, f"Unimplemented: format: {format}"
assert False, format
case SsXXFeatureTag(num=_):
version = read_u16(f)
assert version in [0]
UINameID = read_u16(f)
if version == 0:
return SsXXFeatureParamsTable_Ver_0(version, UINameID)
assert False, f"Unimplemented: version: {version}"
case SimpleFeatureTag.Size:
design_size = read_u16(f)
subfamily_identifier = read_u16(f)
subfamily_nameID = read_u16(f)
if subfamily_identifier != 0: assert 256 <= subfamily_nameID <= 32767
else: assert subfamily_nameID == 0
recommended_usage_range_start = read_u16(f)
recommended_usage_range_end = read_u16(f)
if subfamily_identifier != 0: assert recommended_usage_range_start <= recommended_usage_range_end
else: assert (recommended_usage_range_start, recommended_usage_range_end) == (0, 0)
return SizeFeatureParamsTable(design_size, subfamily_identifier, subfamily_nameID if subfamily_identifier else None, (recommended_usage_range_start, recommended_usage_range_end) if subfamily_identifier else None)
case _:
assert False, f"Unimplemented: featureTag: {featureTag}"
@dataclass
class FeatureTable(Table):
featureParams: Optional[FeatureParamsTable]
lookupIndexCount: int
lookupListIndices: List[int]
def parse_feature_table(f: BinaryIO, featureTag: FeatureTag) -> FeatureTable:
start_tell = f.tell()
featureParamsOffset = read_u16(f)
with SaveTell(f):
featureParams = parse_at_optional_offset(f, start_tell, featureParamsOffset, lambda f: parse_feature_params_table(f, featureTag))
lookupIndexCount = read_u16(f)
lookupListIndices = [read_u16(f) for _ in range(lookupIndexCount)]
return FeatureTable(featureParams, lookupIndexCount, lookupListIndices)
@dataclass
class FeatureRecord:
featureTag: FeatureTag
feature: FeatureTable
def parse_feature_record(f: BinaryIO, start_tell:int) -> FeatureRecord:
featureTag = parse_feature_tag(f)
featureOffset = read_u16(f)
with SaveTell(f):
feature = parse_at_offset(f, start_tell, featureOffset, lambda f: parse_feature_table(f, featureTag))
return FeatureRecord(featureTag, feature)
@dataclass
class FeatureListTable(Table):
featureCount: int
featureRecords: List[FeatureRecord]
def parse_feature_list_table(f: BinaryIO) -> FeatureListTable:
start_tell = f.tell()
featureCount = read_u16(f)
featureRecords = [parse_feature_record(f, start_tell) for _ in range(featureCount)]
return FeatureListTable(featureCount, featureRecords)
LookupType = TypeVar('LookupType', bound=Enum)
@dataclass
class LookupSubtable(Table, ABD): pass
LookupSubtable_ = TypeVar('LookupSubtable_', bound=LookupSubtable)
@dataclass
class LookupFlag: # TODO: Do this like the other flags
rightToLeft: bool
ignoreBaseGlyphs: bool
ignoreLigatures: bool
ignoreMarks: bool
useMarkFilteringSet: bool
markAttachmentType: int
def parse_lookup_flag(f: BinaryIO) -> LookupFlag:
lookupFlag = read_u16(f)
rightToLeft, ignoreBaseGlyphs, ignoreLigatures, ignoreMarks, useMarkFilteringSet = [bool(lookupFlag&(1<<i)) for i in range(5)]
2024-09-15 16:10:41 +10:00
assert lookupFlag & 0x00e0 == 0, "Reserved" # TODO: Once you do this like the other flags, put this in the __post_init__
2024-05-03 21:02:58 +10:00
markAttachmentType = (lookupFlag & 0xff00) >> 8
return LookupFlag(rightToLeft, ignoreBaseGlyphs, ignoreLigatures, ignoreMarks, useMarkFilteringSet, markAttachmentType)
@dataclass
class LookupTable(Table, Generic[LookupType, LookupSubtable_]):
lookupType: LookupType
lookupFlag: LookupFlag
subTableCount: int
subTables: List[LookupSubtable_]
markFilteringSet: Optional[int]
def parse_lookup_table(f: BinaryIO, _lookupType: Callable[[int], LookupType], lookup_subtable_parser: Callable[[BinaryIO, LookupType], LookupSubtable_]) -> LookupTable[LookupType, LookupSubtable_]:
assert isinstance(_lookupType, EnumMeta)
start_tell = f.tell()
2024-05-04 11:34:37 +10:00
lookupType = read_id(f, _lookupType)
2024-05-03 21:02:58 +10:00
lookupFlag = parse_lookup_flag(f)
subTableCount = read_u16(f)
subTableOffsets = [read_u16(f) for _ in range(subTableCount)]
with SaveTell(f):
subTables = parse_at_offsets(f, start_tell, subTableOffsets, lambda f: lookup_subtable_parser(f, lookupType))
markAttachmentType = read_u16(f) if lookupFlag.markAttachmentType else None
return LookupTable(lookupType, lookupFlag, subTableCount, subTables, markAttachmentType)
@dataclass
class LookupListTable(Table, Generic[LookupType, LookupSubtable_]):
lookupCount: int
lookups: List[LookupTable[LookupType, LookupSubtable_]]
def parse_lookup_list_table(f: BinaryIO, lookupType: Callable[[int], LookupType], lookup_subtable_parser: Callable[[BinaryIO, LookupType], LookupSubtable_]) -> LookupListTable[LookupType, LookupSubtable_]:
assert isinstance(lookupType, EnumMeta)
start_tell = f.tell()
lookupCount = read_u16(f)
lookupOffsets = [read_u16(f) for _ in range(lookupCount)]
with SaveTell(f):
lookups = parse_at_offsets(f, start_tell, lookupOffsets, lambda f: parse_lookup_table(f, lookupType, lookup_subtable_parser))
return LookupListTable(lookupCount, lookups)
@dataclass
class ConditionTable(Table, ABD):
format: int
@dataclass
class ConditionTable_Format_1(ConditionTable):
axisIndex: int
filterRangeMinValue: float
filterRangeMaxValue: float
def parse_condition_table(f: BinaryIO) -> ConditionTable:
format = read_u16(f)
assert format in [1]
match format:
case 1:
axisIndex = read_u16(f)
filterRangeMinValue = read_F2DOT14(f)
filterRangeMaxValue = read_F2DOT14(f)
return ConditionTable_Format_1(format, axisIndex, filterRangeMinValue, filterRangeMaxValue)
case _:
assert False, f"Unimplemented: format: {format}"
assert False, format
2024-05-04 11:34:37 +10:00
ConditionSetTable = SetTable[ConditionTable]
2024-05-03 21:02:58 +10:00
def parse_condition_set_table(f: BinaryIO) -> ConditionSetTable:
2024-05-04 11:34:37 +10:00
return parse_set_table(f, parse_condition_table, offset_reader=read_u32)
# start_tell = f.tell()
2024-05-03 21:02:58 +10:00
2024-05-04 11:34:37 +10:00
# conditionCount = read_u16(f)
# conditionOffsets = [read_u32(f) for _ in range(conditionCount)]
# with SaveTell(f):
# conditions = parse_at_offsets(f, start_tell, conditionOffsets, parse_condition_table)
2024-05-03 21:02:58 +10:00
2024-05-04 11:34:37 +10:00
# return ConditionSetTable(conditionCount, conditions)
2024-05-03 21:02:58 +10:00
@dataclass
class FeatureTableSubstitutionRecord:
featureIndex: int
alternateFeatureTable: FeatureTable
def parse_feature_table_substitution_record(f: BinaryIO, start_tell: int, featureList: FeatureListTable) -> FeatureTableSubstitutionRecord:
featureIndex = read_u16(f)
alternateFeatureTableOffset = read_u32(f)
with SaveTell(f):
alternateFeatureTable = parse_at_offset(f, start_tell, alternateFeatureTableOffset, lambda f: parse_feature_table(f, featureList.featureRecords[featureIndex].featureTag)) # TODO: Am I sure that it's the same featureTag? ttf-parser doesn't even know what to do and just passes in b'dflt'
return FeatureTableSubstitutionRecord(featureIndex, alternateFeatureTable)
@dataclass
class FeatureTableSubstitutionTable(Table, ABD):
majorVersion: int
minorVersion: int
substitutionCount: int
substitutions: List[FeatureTableSubstitutionRecord]
@dataclass
class FeatureTableSubstitutionTable_Ver_1_0(FeatureTableSubstitutionTable): pass
def parse_feature_table_substitution_table(f: BinaryIO, featureList: FeatureListTable) -> FeatureTableSubstitutionTable:
start_tell = f.tell()
majorVersion = read_u16(f)
assert majorVersion == 1
minorVersion = read_u16(f)
assert minorVersion in [0]
substitutionCount = read_u16(f)
substitutions = [parse_feature_table_substitution_record(f, start_tell, featureList) for _ in range(substitutionCount)]
if minorVersion == 0:
return FeatureTableSubstitutionTable_Ver_1_0(majorVersion, minorVersion, substitutionCount, substitutions)
assert False, f"Unimplemented: minorVersion: {minorVersion}"
@dataclass
class FeatureVariationRecord:
2024-05-04 11:34:37 +10:00
conditionSet: ConditionSetTable
2024-05-03 21:02:58 +10:00
featureTableSubstitution: FeatureTableSubstitutionTable
def parse_feature_variation_record(f: BinaryIO, start_tell: int, featureList: FeatureListTable) -> FeatureVariationRecord:
conditionSetOffset = read_u32(f)
featureTableSubstitutionOffset = read_u32(f)
with SaveTell(f):
2024-05-04 11:34:37 +10:00
conditionSet = parse_at_offset(f, start_tell, conditionSetOffset, parse_condition_set_table)
2024-05-03 21:02:58 +10:00
featureTableSubstitution = parse_at_offset(f, start_tell, featureTableSubstitutionOffset, lambda f: parse_feature_table_substitution_table(f, featureList))
return FeatureVariationRecord(conditionSet, featureTableSubstitution)
@dataclass
class FeatureVariationsTable(Table, ABD):
majorVersion: int
minorVersion: int
featureVariationRecordCount: int
featureVariationRecords: List[FeatureVariationRecord]
@dataclass
class FeatureVariationsTable_Ver_1_0(FeatureVariationsTable): pass
def parse_feature_variations_table(f: BinaryIO, featureList: FeatureListTable) -> FeatureVariationsTable:
start_tell = f.tell()
majorVersion = read_u16(f)
assert majorVersion == 1
minorVersion = read_u16(f)
assert minorVersion in [0]
featureVariationRecordCount = read_u32(f)
featureVariationRecords = [parse_feature_variation_record(f, start_tell, featureList) for _ in range(featureVariationRecordCount)]
if minorVersion == 0:
return FeatureVariationsTable_Ver_1_0(majorVersion, minorVersion, featureVariationRecordCount, featureVariationRecords)
assert False, f"Unimplemented: minorVersion: {minorVersion}"
@dataclass
class CoverageTable(Table, ABD):
coverageFormat: int
@dataclass
class CoverageTable_Format_1(CoverageTable):
glyphCount: int
glyphArray: List[int]
@dataclass
2024-05-04 11:34:37 +10:00
class RangeRecord(Record):
2024-05-03 21:02:58 +10:00
startGlyphID: int
endGlyphID: int
startCoverageIndex: int
def parse_range_record(f: BinaryIO) -> RangeRecord:
startGlyphID = read_u16(f)
endGlyphID = read_u16(f)
startCoverageIndex = read_u16(f)
return RangeRecord(startGlyphID, endGlyphID, startCoverageIndex)
@dataclass
class CoverageTable_Format_2(CoverageTable):
rangeCount: int
rangeRecords: List[RangeRecord]
def parse_coverage_table(f: BinaryIO) -> CoverageTable:
coverageFormat = read_u16(f)
assert coverageFormat in [1, 2]
match coverageFormat:
case 1:
glyphCount = read_u16(f)
glyphArray = [read_u16(f) for _ in range(glyphCount)]
return CoverageTable_Format_1(coverageFormat, glyphCount, glyphArray)
case 2:
rangeCount = read_u16(f)
rangeRecords = [parse_range_record(f) for _ in range(rangeCount)]
return CoverageTable_Format_2(coverageFormat, rangeCount, rangeRecords)
case _:
assert False, f"Unimplemented: coverageFormat: {coverageFormat}"
@dataclass
class ClassDefTable(Table, ABD):
classFormat: int
@dataclass
class ClassDefTable_Format_1(ClassDefTable):
startGlyphID: int
glyphCount: int
classValueArray: List[int]
@dataclass
class ClassRangeRecord:
startGlyphID: int
endGlyphID: int
classValue: int
2024-05-04 11:34:37 +10:00
def parse_class_range_record(f: BinaryIO) -> ClassRangeRecord:
2024-05-03 21:02:58 +10:00
startGlyphID = read_u16(f)
endGlyphID = read_u16(f)
classValue = read_u16(f)
return ClassRangeRecord(startGlyphID, endGlyphID, classValue)
@dataclass
class ClassDefTable_Format_2(ClassDefTable):
classRangeCount: int
classRangeRecords: List[ClassRangeRecord]
def parse_class_def_table(f: BinaryIO) -> ClassDefTable:
classFormat = read_u16(f)
assert classFormat in [1, 2], f"Invalid classFormat: {classFormat}. Expected 1 or 2."
match classFormat:
case 1:
startGlyphID = read_u16(f)
glyphCount = read_u16(f)
classValueArray = [read_u16(f) for _ in range(glyphCount)]
return ClassDefTable_Format_1(classFormat, startGlyphID, glyphCount, classValueArray)
case 2:
classRangeCount = read_u16(f)
classRangeRecords = [parse_class_range_record(f) for _ in range(classRangeCount)]
return ClassDefTable_Format_2(classFormat, classRangeCount, classRangeRecords)
case _:
assert False, f"Unimplemented: classFormat: {classFormat}"
@dataclass
class ValueFormatFlags:
bytes: int
def x_placement(self) -> bool: return (self.bytes & 0x0001)!=0
def y_placement(self) -> bool: return (self.bytes & 0x0002)!=0
def x_advance(self) -> bool: return (self.bytes & 0x0004)!=0
def y_advance(self) -> bool: return (self.bytes & 0x0008)!=0
def x_placement_device(self) -> bool: return (self.bytes & 0x0010)!=0
def y_placement_device(self) -> bool: return (self.bytes & 0x0020)!=0
def x_advance_device(self) -> bool: return (self.bytes & 0x0040)!=0
def y_advance_device(self) -> bool: return (self.bytes & 0x0080)!=0
2024-09-15 16:10:41 +10:00
def __post_init__(self):
assert self.bytes & 0xFF00 == 0, "Reserved"
2024-05-04 11:34:37 +10:00
def parse_value_format(f: BinaryIO) -> ValueFormatFlags:
2024-05-03 21:02:58 +10:00
valueFormat = read_u16(f)
return ValueFormatFlags(valueFormat)
@dataclass
class DeviceTable(Table):
startSize: int
endSize: int
deltaFormat: int
deltaValue: List[int]
2024-09-15 16:10:41 +10:00
def __post_init__(self):
assert self.deltaFormat & 0x7ffc == 0, "Reserved"
2024-05-03 21:02:58 +10:00
@dataclass
class VariationIndexTable(Table):
deltaSetOuterIndex: int
deltaSetInnerIndex: int
deltaFormat: int
2024-09-15 16:10:41 +10:00
def __post_init__(self):
assert self.deltaFormat == 0x8000
2024-05-03 21:02:58 +10:00
DeviceTable_ = DeviceTable | VariationIndexTable
def parse_device_table(f: BinaryIO) -> DeviceTable_:
first = read_u16(f)
second = read_u16(f)
deltaFormat = read_u16(f)
assert deltaFormat in [1, 2, 3, 0x8000], f"Invalid deltaFormat: {deltaFormat}"
match deltaFormat:
case 1 | 2 | 3:
assert False
case 0x8000:
return VariationIndexTable(first, second, deltaFormat)
case _:
assert False, f"Unimplemented: deltaFormat: {deltaFormat}"
class BaselineTag(Enum):
Hang = 'hang'
Icfb = 'icfb'
Icft = 'icft'
Ideo = 'ideo'
Idtp = 'idtp'
Math = 'math'
Romn = 'romn'
def __str__(self) -> str: return self._value_
@dataclass
class BaseTagListTable(Table):
baseTagCount: int
baselineTags: List[BaselineTag]
def parse_base_tag_list_table(f: BinaryIO) -> BaseTagListTable:
baseTagCount = read_u16(f)
2024-05-04 11:34:37 +10:00
baselineTags = [read_tag(f, BaselineTag) for _ in range(baseTagCount)]
2024-05-03 21:02:58 +10:00
return BaseTagListTable(baseTagCount, baselineTags)
@dataclass
class BaseCoordTable(Table, ABD):
baseCoordFormat: int
@dataclass
class BaseCoordTable_Format_1(BaseCoordTable):
coordinate: int
def parse_base_coord_table(f: BinaryIO) -> BaseCoordTable:
baseCoordFormat = read_u16(f)
assert baseCoordFormat in [1, 2, 3]
match baseCoordFormat:
case 1:
coordinate = read_i16(f)
return BaseCoordTable_Format_1(baseCoordFormat, coordinate)
case _:
assert False, f"Unimplemented: baseCoordFormat: {baseCoordFormat}"
assert False, baseCoordFormat
@dataclass
class BaseValuesTable(Table):
defaultBaselineIndex: int
baseCoordCount: int
baseCoords: List[BaseCoordTable]
def parse_base_values_table(f: BinaryIO) -> BaseValuesTable:
start_tell = f.tell()
defaultBaselineIndex = read_u16(f)
baseCoordCount = read_u16(f)
baseCoordOffsets = [read_u16(f) for _ in range(baseCoordCount)]
with SaveTell(f):
baseCoords = parse_at_offsets(f, start_tell, baseCoordOffsets, parse_base_coord_table)
return BaseValuesTable(defaultBaselineIndex, baseCoordCount, baseCoords)
@dataclass
class MinMaxTable(Table):
pass
def parse_min_max_table(f: BinaryIO) -> MinMaxTable:
assert False
@dataclass
class BaseLangSysRecord:
pass
def parse_base_lang_sys_record(f: BinaryIO) -> BaseLangSysRecord:
assert False
@dataclass
class BaseScriptTable(Table):
baseValues: Optional[BaseValuesTable]
defaultMinMax: Optional[MinMaxTable]
baseLangSysCount: int
baseLangSysRecords: List[BaseLangSysRecord]
def parse_base_script_table(f: BinaryIO) -> BaseScriptTable:
start_tell = f.tell()
baseValuesOffset = read_u16(f)
defaultMinMaxOffset = read_u16(f)
baseLangSysCount = read_u16(f)
baseLangSysRecords = [parse_base_lang_sys_record(f) for _ in range(baseLangSysCount)]
with SaveTell(f):
baseValues = parse_at_optional_offset(f, start_tell, baseValuesOffset, parse_base_values_table)
defaultMinMax = parse_at_optional_offset(f, start_tell, defaultMinMaxOffset, parse_min_max_table)
return BaseScriptTable(baseValues, defaultMinMax, baseLangSysCount, baseLangSysRecords)
@dataclass
class BaseScriptRecord:
baseScriptTag: ScriptTag
baseScript: BaseScriptTable
def parse_base_script_record(f: BinaryIO, start_tell: int) -> BaseScriptRecord:
2024-05-04 11:34:37 +10:00
baseScriptTag = read_tag(f, ScriptTag)
2024-05-03 21:02:58 +10:00
baseScriptOffset = read_u16(f)
with SaveTell(f):
baseScript = parse_at_offset(f, start_tell, baseScriptOffset, parse_base_script_table)
return BaseScriptRecord(baseScriptTag, baseScript)
@dataclass
class BaseScriptListTable(Table):
baseScriptCount: int
baseScriptRecords: List[BaseScriptRecord]
def parse_base_script_list_table(f: BinaryIO) -> BaseScriptListTable:
start_tell = f.tell()
baseScriptCount = read_u16(f)
baseScriptRecords = [parse_base_script_record(f, start_tell) for _ in range(baseScriptCount)]
return BaseScriptListTable(baseScriptCount, baseScriptRecords)
@dataclass
class AxisTable(Table):
baseTagList: Optional[BaseTagListTable]
baseScriptList: BaseScriptListTable
def parse_axis_table(f: BinaryIO) -> AxisTable:
start_tell = f.tell()
baseTagListOffset = read_u16(f)
baseScriptListOffset = read_u16(f)
with SaveTell(f):
baseTagList = parse_at_optional_offset(f, start_tell, baseTagListOffset, parse_base_tag_list_table)
baseScriptList = parse_at_offset(f, start_tell, baseScriptListOffset, parse_base_script_list_table)
return AxisTable(baseTagList, baseScriptList)
@dataclass
class BASETable(Table, ABD):
majorVersion: int
minorVersion: int
horizAxis: Optional[AxisTable]
vertAxis: Optional[AxisTable]
@dataclass
class BASETable_Ver_1_0(BASETable): pass
@dataclass
class BASETable_Ver_1_1(BASETable_Ver_1_0):
itemVarStore: 'Optional[ItemVariationStoreTable]'
def parse_BASE_table(f: BinaryIO) -> BASETable:
start_tell = f.tell()
majorVersion = read_u16(f)
assert majorVersion == 1
minorVersion = read_u16(f)
assert minorVersion in [0, 1]
horizAxisOffset = read_u16(f)
vertAxisOffset = read_u16(f)
with SaveTell(f):
horizAxis = parse_at_optional_offset(f, start_tell, horizAxisOffset, parse_axis_table)
vertAxis = parse_at_optional_offset(f, start_tell, vertAxisOffset, parse_axis_table)
if minorVersion == 0:
return BASETable_Ver_1_0(majorVersion, minorVersion, horizAxis, vertAxis)
assert False, f"Unimplemented: minorVersion: {minorVersion}"
@dataclass
class AttachPointTable(Table):
pointCount: int
pointIndices: List[int]
def parse_attach_point_table(f: BinaryIO) -> AttachPointTable:
pointCount = read_u16(f)
pointIndices = [read_u16(f) for _ in range(pointCount)]
return AttachPointTable(pointCount, pointIndices)
@dataclass
class AttachPointListTable(Table):
coverage: CoverageTable
glyphCount: int
attachPoints: List[AttachPointTable]
def parse_attach_point_list_table(f: BinaryIO) -> AttachPointListTable:
start_tell = f.tell()
coverageOffset = read_u16(f)
glyphCount = read_u16(f)
attachPointOffsets = [read_u16(f) for _ in range(glyphCount)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
attachPoints = parse_at_offsets(f, start_tell, attachPointOffsets, parse_attach_point_table)
return AttachPointListTable(coverage, glyphCount, attachPoints)
@dataclass
class CaretValueTable(Table, ABD):
caretValueFormat: int
@dataclass
class CaretValueTable_Format_1(CaretValueTable):
coordinate: int
def parse_caret_value_table(f: BinaryIO) -> CaretValueTable:
caretValueFormat = read_u16(f)
assert caretValueFormat in [1, 2, 3]
match caretValueFormat:
case 1:
coordinate = read_i16(f)
return CaretValueTable_Format_1(caretValueFormat, coordinate)
case _:
assert False, f"Unimplemented: caretValueFormat: {caretValueFormat}"
assert False, caretValueFormat
@dataclass
class LigGlyphTable(Table):
caretCount: int
caretValues: List[CaretValueTable]
def parse_lig_glyph_table(f: BinaryIO) -> LigGlyphTable:
start_tell = f.tell()
caretCount = read_u16(f)
caretValueOffsets = [read_u16(f) for _ in range(caretCount)]
with SaveTell(f):
caretValues = parse_at_offsets(f, start_tell, caretValueOffsets, parse_caret_value_table)
return LigGlyphTable(caretCount, caretValues)
@dataclass
class LigCaretListTable(Table):
coverage: CoverageTable
ligGlyphCount: int
ligGlyphs: List[LigGlyphTable]
def parse_lig_caret_list_table(f: BinaryIO) -> LigCaretListTable:
start_tell = f.tell()
coverageOffset = read_u16(f)
ligGlyphCount = read_u16(f)
ligGlyphOffsets = [read_u16(f) for _ in range(ligGlyphCount)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
ligGlyphs = parse_at_offsets(f, start_tell, ligGlyphOffsets, parse_lig_glyph_table)
return LigCaretListTable(coverage, ligGlyphCount, ligGlyphs)
@dataclass
class GDEFTable(Table, ABD):
majorVersion: int
minorVersion: int
glyphClassDef: Optional[ClassDefTable]
attachList: Optional[AttachPointListTable]
ligCaretList: Optional[LigCaretListTable]
markAttachClassDef: Optional[ClassDefTable]
@dataclass
class GDEFTable_Ver_1_0(GDEFTable): pass
@dataclass
class MarkGlyphSetsTable(Table):
markGlyphSetTableFormat: int
markGlyphSetCount: int
coverages: List[CoverageTable]
def parse_mark_glyph_sets_table(f: BinaryIO) -> MarkGlyphSetsTable:
start_tell = f.tell()
markGlyphSetTableFormat = read_u16(f)
assert markGlyphSetTableFormat == 1
markGlyphSetCount = read_u16(f)
coverageOffsets = [read_u32(f) for _ in range(markGlyphSetCount)]
with SaveTell(f):
coverages = parse_at_offsets(f, start_tell, coverageOffsets, parse_coverage_table)
return MarkGlyphSetsTable(markGlyphSetTableFormat, markGlyphSetCount, coverages)
@dataclass
class GDEFTable_Ver_1_2(GDEFTable_Ver_1_0):
markGlyphSetsDef: Optional[MarkGlyphSetsTable]
@dataclass
class GDEFTable_Ver_1_3(GDEFTable_Ver_1_2):
itemVarStore: 'Optional[ItemVariationStoreTable]'
def parse_GDEF_table(f: BinaryIO) -> GDEFTable:
start_tell = f.tell()
majorVersion = read_u16(f)
assert majorVersion == 1
minorVersion = read_u16(f)
assert minorVersion in [0, 2, 3], f"Invalid minorVersion: {minorVersion}. Expected 0, 2, or 3."
glyphClassDefOffset = read_u16(f)
attachListOffset = read_u16(f)
ligCaretListOffset = read_u16(f)
markAttachClassDefOffset = read_u16(f)
with SaveTell(f):
glyphClassDef = parse_at_optional_offset(f, start_tell, glyphClassDefOffset, parse_class_def_table)
attachList = parse_at_optional_offset(f, start_tell, attachListOffset, parse_attach_point_list_table)
ligCaretList = parse_at_optional_offset(f, start_tell, ligCaretListOffset, parse_lig_caret_list_table)
markAttachClassDef = parse_at_optional_offset(f, start_tell, markAttachClassDefOffset, parse_class_def_table)
if minorVersion == 0:
return GDEFTable_Ver_1_0(majorVersion, minorVersion, glyphClassDef, attachList, ligCaretList, markAttachClassDef)
markGlyphSetsDefOffset = read_u16(f)
with SaveTell(f):
markGlyphSetsDef = parse_at_optional_offset(f, start_tell, markGlyphSetsDefOffset, parse_mark_glyph_sets_table)
if minorVersion == 2:
return GDEFTable_Ver_1_2(majorVersion, minorVersion, glyphClassDef, attachList, ligCaretList, markAttachClassDef, markGlyphSetsDef)
itemVarStoreOffset = read_u32(f)
with SaveTell(f):
itemVarStore = parse_at_optional_offset(f, start_tell, itemVarStoreOffset, parse_item_variation_store_table)
if minorVersion == 3:
return GDEFTable_Ver_1_3(majorVersion, minorVersion, glyphClassDef, attachList, ligCaretList, markAttachClassDef, markGlyphSetsDef, itemVarStore)
assert False, f"Unimplemented: minorVersion: {minorVersion}"
@dataclass
class ValueRecord:
xPlacement: Optional[int]
yPlacement: Optional[int]
xAdvance: Optional[int]
yAdvance: Optional[int]
xPlaDevice: Optional[DeviceTable_]
yPlaDevice: Optional[DeviceTable_]
xAdvDevice: Optional[DeviceTable_]
yAdvDevice: Optional[DeviceTable_]
def parse_value_record(f: BinaryIO, start_tell: int, valueFormat: ValueFormatFlags) -> ValueRecord:
xPlacement = read_i16(f) if valueFormat.x_placement() else None
yPlacement = read_i16(f) if valueFormat.y_placement() else None
xAdvance = read_i16(f) if valueFormat.x_advance() else None
yAdvance = read_i16(f) if valueFormat.y_advance() else None
2024-09-15 16:10:41 +10:00
xPlaDeviceOffset = read_u16(f) if valueFormat.x_placement_device() else 0
yPlaDeviceOffset = read_u16(f) if valueFormat.y_placement_device() else 0
xAdvDeviceOffset = read_u16(f) if valueFormat.x_advance_device() else 0
yAdvDeviceOffset = read_u16(f) if valueFormat.y_advance_device() else 0
2024-05-03 21:02:58 +10:00
with SaveTell(f):
xPlaDevice = parse_at_optional_offset(f, start_tell, xPlaDeviceOffset, parse_device_table)
yPlaDevice = parse_at_optional_offset(f, start_tell, yPlaDeviceOffset, parse_device_table)
xAdvDevice = parse_at_optional_offset(f, start_tell, xAdvDeviceOffset, parse_device_table)
yAdvDevice = parse_at_optional_offset(f, start_tell, yAdvDeviceOffset, parse_device_table)
return ValueRecord(xPlacement, yPlacement, xAdvance, yAdvance, xPlaDevice, yPlaDevice, xAdvDevice, yAdvDevice)
@dataclass
class AnchorTable(Table, ABD):
anchorFormat: int
@dataclass
class AnchorTable_Format_1(AnchorTable):
xCoordinate: int
yCoordinate: int
@dataclass
class AnchorTable_Format_2(AnchorTable):
xCoordinate: int
yCoordinate: int
anchorPoint: int
@dataclass
class AnchorTable_Format_3(AnchorTable):
xCoordinate: int
yCoordinate: int
xDevice: Optional[DeviceTable_]
yDevice: Optional[DeviceTable_]
def parse_anchor_table(f: BinaryIO) -> AnchorTable:
start_tell = f.tell()
anchorFormat = read_u16(f)
assert anchorFormat in [1, 2, 3], f"Invalid anchorFormat: {anchorFormat}"
match anchorFormat:
case 1:
xCoordinate = read_i16(f)
yCoordinate = read_i16(f)
return AnchorTable_Format_1(anchorFormat, xCoordinate, yCoordinate)
case 2:
xCoordinate = read_i16(f)
yCoordinate = read_i16(f)
anchorPoint = read_u16(f)
return AnchorTable_Format_2(anchorFormat, xCoordinate, yCoordinate, anchorPoint)
case 3:
xCoordinate = read_i16(f)
yCoordinate = read_i16(f)
xDeviceOffset = read_u16(f)
yDeviceOffset = read_u16(f)
with SaveTell(f):
xDevice = parse_at_optional_offset(f, start_tell, xDeviceOffset, parse_device_table)
yDevice = parse_at_optional_offset(f, start_tell, yDeviceOffset, parse_device_table)
return AnchorTable_Format_3(anchorFormat, xCoordinate, yCoordinate, xDevice, yDevice)
case _:
assert False, f"Unimplemented: anchorFormat: {anchorFormat}"
assert False
class GPOSLookupType(Enum):
SinglePos = 1
PairPos = 2
CursivePos = 3
MarkBasePos = 4
MarkLigPos = 5
MarkMarkPos = 6
ContextPos = 7
ChainContextPos = 8
ExtensionPos = 9
def __str__(self) -> str: return self._name_
@dataclass
class GPOSLookupSubtable(LookupSubtable, ABD): pass
# 1
@dataclass
class SinglePosSubtable(GPOSLookupSubtable, ABD):
posFormat: int
@dataclass
class SinglePosSubtable_Format_1(SinglePosSubtable):
coverage: CoverageTable
valueFormat: ValueFormatFlags
valueRecord: ValueRecord
@dataclass
class SinglePosSubtable_Format_2(SinglePosSubtable):
coverage: CoverageTable
valueFormat: ValueFormatFlags
valueCount: int
valueRecords: List[ValueRecord]
# 2
@dataclass
class PairPosSubtable(GPOSLookupSubtable, ABD):
posFormat: int
@dataclass
class PairValueRecord:
secondGlyph: int
valueRecord1: ValueRecord
valueRecord2: ValueRecord
def parse_pair_value_record(f: BinaryIO, start_tell: int, valueFormat1: ValueFormatFlags, valueFormat2: ValueFormatFlags) -> PairValueRecord:
secondGlyph = read_u16(f)
valueRecord1 = parse_value_record(f, start_tell, valueFormat1)
valueRecord2 = parse_value_record(f, start_tell, valueFormat2)
return PairValueRecord(secondGlyph, valueRecord1, valueRecord2)
@dataclass
class PairSetTable(Table):
pairValueCount: int
pairValueRecords: List[PairValueRecord]
def parse_pair_set_table(f: BinaryIO, start_tell: int, valueFormat1: ValueFormatFlags, valueFormat2: ValueFormatFlags) -> PairSetTable:
"""
`start_tell` should be the start of the PairSetTable, so pass in `f.tell()`
It's a parameter because I might be wrong, so there is always the option to pass in another `start_tell`.
"""
# start_tell = f.tell()
pairValueCount = read_u16(f)
pairValueRecords = [parse_pair_value_record(f, start_tell, valueFormat1, valueFormat2) for _ in range(pairValueCount)]
return PairSetTable(pairValueCount, pairValueRecords)
@dataclass
class PairPosSubtable_Format_1(PairPosSubtable):
coverage: CoverageTable
valueFormat1: ValueFormatFlags
valueFormat2: ValueFormatFlags
pairSetCount: int
pairSets: List[PairSetTable]
@dataclass
class Class2Record:
valueRecord1: ValueRecord
valueRecord2: ValueRecord
def parse_class2_record(f: BinaryIO, start_tell: int, valueFormat1: ValueFormatFlags, valueFormat2: ValueFormatFlags) -> Class2Record:
valueRecord1 = parse_value_record(f, start_tell, valueFormat1)
valueRecord2 = parse_value_record(f, start_tell, valueFormat2)
return Class2Record(valueRecord1, valueRecord2)
@dataclass
class Class1Record:
class2Records: List[Class2Record]
def parse_class1_record(f: BinaryIO, start_tell: int, class2Count: int, valueFormat1: ValueFormatFlags, valueFormat2: ValueFormatFlags) -> Class1Record:
class2Records = [parse_class2_record(f, start_tell, valueFormat1, valueFormat2) for _ in range(class2Count)]
return Class1Record(class2Records)
@dataclass
class PairPosSubtable_Format_2(PairPosSubtable):
coverage: CoverageTable
valueFormat1: ValueFormatFlags
valueFormat2: ValueFormatFlags
classDef1: ClassDefTable
classDef2: ClassDefTable
class1Count: int
class2Count: int
class1Record: List[Class1Record]
# 3
@dataclass
class CursivePosSubtable(GPOSLookupSubtable, ABD):
posFormat: int
@dataclass
class EntryExitRecord:
entryAnchor: Optional[AnchorTable]
exitAnchor: Optional[AnchorTable]
def parse_entry_exit_record(f: BinaryIO, start_tell: int) -> EntryExitRecord:
entryAnchorOffset = read_u16(f)
exitAnchorOffset = read_u16(f)
with SaveTell(f):
entryAnchor = parse_at_optional_offset(f, start_tell, entryAnchorOffset, parse_anchor_table)
exitAnchor = parse_at_optional_offset(f, start_tell, exitAnchorOffset, parse_anchor_table)
return EntryExitRecord(entryAnchor, exitAnchor)
@dataclass
class CursivePosSubtable_Format_1(CursivePosSubtable):
coverage: CoverageTable
entryExitCount: int
entryExitRecord: List[EntryExitRecord]
# 4?
@dataclass
class MarkRecord:
markClass: int
markAnchor: AnchorTable
def parse_mark_record(f: BinaryIO, start_tell:int) -> MarkRecord:
markClass = read_u16(f)
markAnchorOffset = read_u16(f)
with SaveTell(f):
markAnchor = parse_at_offset(f, start_tell, markAnchorOffset, parse_anchor_table)
return MarkRecord(markClass, markAnchor)
@dataclass
class MarkArrayTable(Table):
markCount: int
markRecords: List[MarkRecord]
def parse_mark_array_table(f: BinaryIO) -> MarkArrayTable:
start_tell = f.tell()
markCount = read_u16(f)
markRecords = [parse_mark_record(f, start_tell) for _ in range(markCount)]
return MarkArrayTable(markCount, markRecords)
@dataclass
class MarkBasePosSubtable(GPOSLookupSubtable, ABD):
posFormat: int
@dataclass
class BaseRecord:
baseAnchor: List[Optional[AnchorTable]] # ISO Documentation doesn't say that this can be null, but Microsoft documentation does, and the file I'm using to test agrees with Microsoft, so...
def parse_base_record(f: BinaryIO, markClassCount:int, start_tell:int) -> BaseRecord:
baseAnchorOffsets = [read_u16(f) for _ in range(markClassCount)]
with SaveTell(f):
baseAnchor = parse_at_optional_offsets(f, start_tell, baseAnchorOffsets, parse_anchor_table)
return BaseRecord(baseAnchor)
@dataclass
class BaseArrayTable(Table):
baseCount: int
baseRecords: List[BaseRecord]
def parse_base_array_table(f: BinaryIO, markClassCount: int) -> BaseArrayTable:
start_tell = f.tell()
baseCount = read_u16(f)
baseRecords = [parse_base_record(f, markClassCount, start_tell) for _ in range(baseCount)]
return BaseArrayTable(baseCount, baseRecords)
# TODO: Maybe don't reparse the same region if they're all pointing to the same place in the file?
@dataclass
class MarkBasePosSubtable_Format_1(MarkBasePosSubtable):
markCoverage: CoverageTable
baseCoverage: CoverageTable
markClassCount: int
markArray: MarkArrayTable
baseArray: BaseArrayTable
# 5
@dataclass
class MarkLigPosSubtable(GPOSLookupSubtable, ABD):
posFormat: int
@dataclass
class ComponentRecord:
ligatureAnchors: List[AnchorTable]
def parse_component_record(f: BinaryIO, start_tell: int, markClassCount: int) -> ComponentRecord:
ligatureAnchorOffsets = [read_u16(f) for _ in range(markClassCount)]
with SaveTell(f):
ligatureAnchors = parse_at_offsets(f, start_tell, ligatureAnchorOffsets, parse_anchor_table)
return ComponentRecord(ligatureAnchors)
@dataclass
class LigatureAttachTable(Table):
componentCount: int
componentRecords: List[ComponentRecord]
def parse_ligature_attach_table(f: BinaryIO, markClassCount: int) -> LigatureAttachTable:
start_tell = f.tell()
componentCount = read_u16(f)
componentRecords = [parse_component_record(f, start_tell, markClassCount) for _ in range(componentCount)]
return LigatureAttachTable(componentCount, componentRecords)
@dataclass
class LigatureArrayTable(Table):
ligatureCount: int
ligatureAttachs: List[LigatureAttachTable]
def parse_ligature_array_table(f: BinaryIO, markClassCount: int) -> LigatureArrayTable:
start_tell = f.tell()
ligatureCount = read_u16(f)
ligatureAttachOffsets = [read_u16(f) for _ in range(ligatureCount)]
with SaveTell(f):
ligatureAttachs = parse_at_offsets(f, start_tell, ligatureAttachOffsets, lambda f: parse_ligature_attach_table(f, markClassCount))
return LigatureArrayTable(ligatureCount, ligatureAttachs)
@dataclass
class MarkLigPosSubtable_Format_1(MarkLigPosSubtable):
markCoverage: CoverageTable
ligatureCoverage: CoverageTable
markClassCount: int
markArray: MarkArrayTable
ligatureArray: LigatureArrayTable
# 6
@dataclass
class MarkMarkPosSubtable(GPOSLookupSubtable, ABD):
posFormat: int
@dataclass
class Mark2Record:
mark2Anchor: List[Optional[AnchorTable]]
def parse_mark2_record(f: BinaryIO, markClassCount:int, start_tell:int) -> Mark2Record:
mark2AnchorOffsets = [read_u16(f) for _ in range(markClassCount)]
with SaveTell(f):
mark2Anchor = parse_at_optional_offsets(f, start_tell, mark2AnchorOffsets, parse_anchor_table)
return Mark2Record(mark2Anchor)
@dataclass
class Mark2ArrayTable(Table):
mark2Count: int
mark2Records: List[Mark2Record]
def parse_mark2_array_table(f: BinaryIO, markClassCount:int) -> Mark2ArrayTable:
start_tell = f.tell()
mark2Count = read_u16(f)
mark2Records = [parse_mark2_record(f, markClassCount, start_tell) for _ in range(mark2Count)]
return Mark2ArrayTable(mark2Count, mark2Records)
@dataclass
class MarkMarkPosSubtable_Format_1(MarkMarkPosSubtable):
mark1Coverage: CoverageTable
mark2Coverage: CoverageTable
markClassCount: int
mark1Array: MarkArrayTable
mark2Array: Mark2ArrayTable
# 7
@dataclass
class ContextPosSubtable(GPOSLookupSubtable, ABD):
posFormat: int
2024-05-03 21:02:58 +10:00
@dataclass
class PosLookupRecord:
sequenceIndex: int
lookupListIndex: int
def parse_pos_lookup_record(f: BinaryIO) -> PosLookupRecord:
sequenceIndex = read_u16(f)
lookupListIndex = read_u16(f)
return PosLookupRecord(sequenceIndex, lookupListIndex)
@dataclass
class PosRuleTable(Table):
glyphCount: int
posCount: int
inputSequence: List[int]
posLookupRecords: List[PosLookupRecord]
def parse_pos_rule_table(f: BinaryIO) -> PosRuleTable:
glyphCount = read_u16(f)
posCount = read_u16(f)
inputSequence = [read_u16(f) for _ in range(glyphCount-1)]
posLookupRecords = [parse_pos_lookup_record(f) for _ in range(posCount)]
return PosRuleTable(glyphCount, posCount, inputSequence, posLookupRecords)
PosRuleSetTable = SetTable[PosRuleTable]
def parse_pos_rule_set_table(f: BinaryIO) -> PosRuleSetTable:
return parse_set_table(f, parse_pos_rule_table)
@dataclass
class ContextPosSubtable_Format_1(ContextPosSubtable):
coverage: CoverageTable
posRuleSetCount: int
posRuleSets: List[PosRuleSetTable]
@dataclass
class PosClassRuleTable(Table):
glyphCount: int
posCount: int
classes: List[int]
posLookupRecords: List[PosLookupRecord]
def parse_pos_class_rule_table(f: BinaryIO) -> PosClassRuleTable:
glyphCount = read_u16(f)
posCount = read_u16(f)
classes = [read_u16(f) for _ in range(glyphCount-1)]
posLookupRecords = [parse_pos_lookup_record(f) for _ in range(posCount)]
return PosClassRuleTable(glyphCount, posCount, classes, posLookupRecords)
PosClassSetTable = SetTable[PosClassRuleTable]
def parse_pos_class_set_table(f: BinaryIO) -> PosClassSetTable:
return parse_set_table(f, parse_pos_class_rule_table)
@dataclass
class ContextPosSubtable_Format_2(ContextPosSubtable):
coverage: CoverageTable
classDef: ClassDefTable
posClassSetCount: int
posClassSets: List[Optional[PosClassSetTable]]
2024-05-03 21:02:58 +10:00
# 8
@dataclass
class ChainContextPosSubtable(GPOSLookupSubtable, ABD):
posFormat: int
@dataclass
class ChainPosRuleTable(Table):
backtrackGlyphCount: int
backtrackSequence: List[int]
inputGlyphCount: int
inputSequence: List[int]
lookaheadGlyphCount: int
lookAheadSequence: List[int]
posCount: int
posLookupRecords: List[PosLookupRecord]
def parse_chain_pos_rule_table(f: BinaryIO) -> ChainPosRuleTable:
backtrackGlyphCount = read_u16(f)
backtrackSequence = [read_u16(f) for _ in range(backtrackGlyphCount)]
inputGlyphCount = read_u16(f)
inputSequence = [read_u16(f) for _ in range(inputGlyphCount-1)]
lookaheadGlyphCount = read_u16(f)
lookAheadSequence = [read_u16(f) for _ in range(lookaheadGlyphCount)]
posCount = read_u16(f)
posLookupRecords = [parse_pos_lookup_record(f) for _ in range(posCount)]
return ChainPosRuleTable(backtrackGlyphCount, backtrackSequence, inputGlyphCount, inputSequence, lookaheadGlyphCount, lookAheadSequence, posCount, posLookupRecords)
2024-05-04 11:34:37 +10:00
ChainPosRuleSetTable = SetTable[ChainPosRuleTable]
def parse_chain_pos_rule_set_table(f: BinaryIO) -> ChainPosRuleSetTable:
return parse_set_table(f, parse_chain_pos_rule_table)
2024-05-03 21:02:58 +10:00
@dataclass
class ChainContextPosSubtable_Format_1(ChainContextPosSubtable):
coverage: CoverageTable
chainPosRuleSetCount: int
2024-05-04 11:34:37 +10:00
chainPosRuleSets: List[ChainPosRuleSetTable]
2024-05-03 21:02:58 +10:00
@dataclass
class ChainPosClassRuleTable(Table):
backtrackGlyphCount: int
backtrackSequence: List[int]
inputGlyphCount: int
inputSequence: List[int]
lookaheadGlyphCount: int
lookAheadSequence: List[int]
posCount: int
posLookupRecords: List[PosLookupRecord]
def parse_chain_pos_class_rule_table(f: BinaryIO) -> ChainPosClassRuleTable:
backtrackGlyphCount = read_u16(f)
backtrackSequence = [read_u16(f) for _ in range(backtrackGlyphCount)]
inputGlyphCount = read_u16(f)
inputSequence = [read_u16(f) for _ in range(inputGlyphCount-1)]
lookaheadGlyphCount = read_u16(f)
lookAheadSequence = [read_u16(f) for _ in range(lookaheadGlyphCount)]
posCount = read_u16(f)
posLookupRecords = [parse_pos_lookup_record(f) for _ in range(posCount)]
return ChainPosClassRuleTable(backtrackGlyphCount, backtrackSequence, inputGlyphCount, inputSequence, lookaheadGlyphCount, lookAheadSequence, posCount, posLookupRecords)
2024-05-04 11:34:37 +10:00
ChainPosClassRuleSetTable = SetTable[ChainPosClassRuleTable]
def parse_chain_pos_class_rule_set_table(f: BinaryIO) -> ChainPosClassRuleSetTable:
return parse_set_table(f, parse_chain_pos_class_rule_table)
2024-05-03 21:02:58 +10:00
@dataclass
class ChainContextPosSubtable_Format_2(ChainContextPosSubtable):
coverage: CoverageTable
backtrackClassDef: ClassDefTable
inputClassDef: ClassDefTable
lookaheadClassDef: ClassDefTable
chainPosClassSetCount: int
2024-05-04 11:34:37 +10:00
chainPosClassSets: List[Optional[ChainPosClassRuleSetTable]]
2024-05-03 21:02:58 +10:00
@dataclass
class ChainContextPosSubtable_Format_3(ChainContextPosSubtable):
backtrackGlyphCount: int
backtrackCoverages: List[CoverageTable]
inputGlyphCount: int
inputCoverages: List[CoverageTable]
lookaheadGlyphCount: int
lookaheadCoverages: List[CoverageTable]
posCount: int
posLookupRecords: List[PosLookupRecord]
# 9
@dataclass
class ExtensionPosSubtable(GPOSLookupSubtable, ABD):
posFormat: int
@dataclass
class ExtensionPosSubtable_Format_1(ExtensionPosSubtable):
extensionLookupType: GPOSLookupType
extension: GPOSLookupSubtable
def parse_GPOS_lookup_subtable(f: BinaryIO, lookupType: GPOSLookupType) -> GPOSLookupSubtable:
match lookupType:
case GPOSLookupType.SinglePos:
start_tell = f.tell()
posFormat = read_u16(f)
assert posFormat in [1, 2]
match posFormat:
case 1:
coverageOffset = read_u16(f)
valueFormat = parse_value_format(f)
valueRecord = parse_value_record(f, start_tell, valueFormat)
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
return SinglePosSubtable_Format_1(posFormat, coverage, valueFormat, valueRecord)
case 2:
coverageOffset = read_u16(f)
valueFormat = parse_value_format(f)
valueCount = read_u16(f)
valueRecords = [parse_value_record(f, start_tell, valueFormat) for _ in range(valueCount)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
return SinglePosSubtable_Format_2(posFormat, coverage, valueFormat, valueCount, valueRecords)
case _:
assert False, f"Unimplemented: posFormat: {posFormat}"
assert False, posFormat
case GPOSLookupType.PairPos:
start_tell = f.tell()
posFormat = read_u16(f)
assert posFormat in [1, 2]
match posFormat:
case 1:
coverageOffset = read_u16(f)
valueFormat1 = parse_value_format(f)
valueFormat2 = parse_value_format(f)
pairSetCount = read_u16(f)
pairSetOffsets = [read_u16(f) for _ in range(pairSetCount)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
# I think that the documentation for the Device tables located in these PairSetTables is wrong.
# ISO Documentation says that the offsets are from the start of the PairPos Subtable,
# but I think that it is actually from the start of the PairSet table.
# ttf-parser also seems to agree.
pairSets = parse_at_offsets(f, start_tell, pairSetOffsets, lambda f: parse_pair_set_table(f, f.tell(), valueFormat1, valueFormat2))
return PairPosSubtable_Format_1(posFormat, coverage, valueFormat1, valueFormat2, pairSetCount, pairSets)
case 2:
coverageOffset = read_u16(f)
valueFormat1 = parse_value_format(f)
valueFormat2 = parse_value_format(f)
classDef1Offset = read_u16(f)
classDef2Offset = read_u16(f)
class1Count = read_u16(f)
class2Count = read_u16(f)
class1Record = [parse_class1_record(f, start_tell, class2Count, valueFormat1, valueFormat2) for _ in range(class1Count)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
classDef1 = parse_at_offset(f, start_tell, classDef1Offset, parse_class_def_table)
classDef2 = parse_at_offset(f, start_tell, classDef2Offset, parse_class_def_table)
return PairPosSubtable_Format_2(posFormat, coverage, valueFormat1, valueFormat2, classDef1, classDef2, class1Count, class2Count, class1Record)
case _:
assert False, f"Unimplemented: posFormat: {posFormat}"
assert False, posFormat
case GPOSLookupType.CursivePos:
start_tell = f.tell()
posFormat = read_u16(f)
assert posFormat in [1]
match posFormat:
case 1:
coverageOffset = read_u16(f)
entryExitCount = read_u16(f)
entryExitRecord = [parse_entry_exit_record(f, start_tell) for _ in range(entryExitCount)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
return CursivePosSubtable_Format_1(posFormat, coverage, entryExitCount, entryExitRecord)
case _:
assert False, f"Unimplemented: posFormat: {posFormat}"
assert False, posFormat
case GPOSLookupType.MarkBasePos:
start_tell = f.tell()
posFormat = read_u16(f)
assert posFormat in [1]
match posFormat:
case 1:
markCoverageOffset = read_u16(f)
baseCoverageOffset = read_u16(f)
markClassCount = read_u16(f)
markArrayOffset = read_u16(f)
baseArrayOffset = read_u16(f)
with SaveTell(f):
markCoverage = parse_at_offset(f, start_tell, markCoverageOffset, parse_coverage_table)
baseCoverage = parse_at_offset(f, start_tell, baseCoverageOffset, parse_coverage_table)
markArray = parse_at_offset(f, start_tell, markArrayOffset, parse_mark_array_table)
baseArray = parse_at_offset(f, start_tell, baseArrayOffset, lambda f: parse_base_array_table(f, markClassCount))
return MarkBasePosSubtable_Format_1(posFormat, markCoverage, baseCoverage, markClassCount, markArray, baseArray)
case _:
assert False, f"Unimplemented: posFormat: {posFormat}"
assert False, posFormat
case GPOSLookupType.MarkLigPos:
start_tell = f.tell()
posFormat = read_u16(f)
assert posFormat in [1]
match posFormat:
case 1:
markCoverageOffset = read_u16(f)
ligatureCoverageOffset = read_u16(f)
markClassCount = read_u16(f)
markArrayOffset = read_u16(f)
ligatureArrayOffset = read_u16(f)
with SaveTell(f):
markCoverage = parse_at_offset(f, start_tell, markCoverageOffset, parse_coverage_table)
ligatureCoverage = parse_at_offset(f, start_tell, ligatureCoverageOffset, parse_coverage_table)
markArray = parse_at_offset(f, start_tell, markArrayOffset, parse_mark_array_table)
ligatureArray = parse_at_offset(f, start_tell, ligatureArrayOffset, lambda f: parse_ligature_array_table(f, markClassCount))
return MarkLigPosSubtable_Format_1(posFormat, markCoverage, ligatureCoverage, markClassCount, markArray, ligatureArray)
case _:
assert False, f"Unimplemented: posFormat: {posFormat}"
assert False, posFormat
case GPOSLookupType.MarkMarkPos:
start_tell = f.tell()
posFormat = read_u16(f)
assert posFormat in [1]
match posFormat:
case 1:
mark1CoverageOffset = read_u16(f)
mark2CoverageOffset = read_u16(f)
markClassCount = read_u16(f)
mark1ArrayOffset = read_u16(f)
mark2ArrayOffset = read_u16(f)
with SaveTell(f):
mark1Coverage = parse_at_offset(f, start_tell, mark1CoverageOffset, parse_coverage_table)
mark2Coverage = parse_at_offset(f, start_tell, mark2CoverageOffset, parse_coverage_table)
mark1Array = parse_at_offset(f, start_tell, mark1ArrayOffset, parse_mark_array_table)
mark2Array = parse_at_offset(f, start_tell, mark2ArrayOffset, lambda f: parse_mark2_array_table(f, markClassCount))
return MarkMarkPosSubtable_Format_1(posFormat, mark1Coverage, mark2Coverage, markClassCount, mark1Array, mark2Array)
case _:
assert False, f"Unimplemented: posFormat: {posFormat}"
assert False, posFormat
case GPOSLookupType.ContextPos:
start_tell = f.tell()
posFormat = read_u16(f)
assert posFormat in [1, 2, 3]
match posFormat:
case 1:
coverageOffset = read_u16(f)
posRuleSetCount = read_u16(f)
posRuleSetOffsets = [read_u16(f) for _ in range(posRuleSetCount)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
posRuleSets = parse_at_offsets(f, start_tell, posRuleSetOffsets, parse_pos_rule_set_table)
return ContextPosSubtable_Format_1(posFormat, coverage, posRuleSetCount, posRuleSets)
case 2:
coverageOffset = read_u16(f)
classDefOffset = read_u16(f)
posClassSetCount = read_u16(f)
posClassSetOffsets = [read_u16(f) for _ in range(posClassSetCount)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
classDef = parse_at_offset(f, start_tell, classDefOffset, parse_class_def_table)
posClassSets = parse_at_optional_offsets(f, start_tell, posClassSetOffsets, parse_pos_class_set_table)
return ContextPosSubtable_Format_2(posFormat, coverage, classDef, posClassSetCount, posClassSets)
case _:
assert False, f"Unimplemented: posFormat: {posFormat}"
2024-05-03 21:02:58 +10:00
assert False, posFormat
case GPOSLookupType.ChainContextPos:
start_tell = f.tell()
posFormat = read_u16(f)
assert posFormat in [1, 2, 3]
match posFormat:
case 1:
coverageOffset = read_u16(f)
chainPosRuleSetCount = read_u16(f)
chainPosRuleSetOffsets = [read_u16(f) for _ in range(chainPosRuleSetCount)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
2024-05-04 11:34:37 +10:00
chainPosRuleSets = parse_at_offsets(f, start_tell, chainPosRuleSetOffsets, parse_chain_pos_rule_set_table)
2024-05-03 21:02:58 +10:00
return ChainContextPosSubtable_Format_1(posFormat, coverage, chainPosRuleSetCount, chainPosRuleSets)
case 2:
coverageOffset = read_u16(f)
backtrackClassDefOffset = read_u16(f)
inputClassDefOffset = read_u16(f)
lookaheadClassDefOffset = read_u16(f)
chainPosClassSetCount = read_u16(f)
chainPosClassSetOffsets = [read_u16(f) for _ in range(chainPosClassSetCount)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
backtrackClassDef = parse_at_offset(f, start_tell, backtrackClassDefOffset, parse_class_def_table)
inputClassDef = parse_at_offset(f, start_tell, inputClassDefOffset, parse_class_def_table)
lookaheadClassDef = parse_at_offset(f, start_tell, lookaheadClassDefOffset, parse_class_def_table)
2024-05-04 11:34:37 +10:00
chainPosClassSets = parse_at_optional_offsets(f, start_tell, chainPosClassSetOffsets, parse_chain_pos_class_rule_set_table)
2024-05-03 21:02:58 +10:00
return ChainContextPosSubtable_Format_2(posFormat, coverage, backtrackClassDef, inputClassDef, lookaheadClassDef, chainPosClassSetCount, chainPosClassSets)
case 3:
backtrackGlyphCount = read_u16(f)
backtrackCoverageOffsets = [read_u16(f) for _ in range(backtrackGlyphCount)]
inputGlyphCount = read_u16(f)
inputCoverageOffsets = [read_u16(f) for _ in range(inputGlyphCount)]
lookaheadGlyphCount = read_u16(f)
lookaheadCoverageOffsets = [read_u16(f) for _ in range(lookaheadGlyphCount)]
posCount = read_u16(f)
posLookupRecords = [parse_pos_lookup_record(f) for _ in range(posCount)]
with SaveTell(f):
backtrackCoverages = parse_at_offsets(f, start_tell, backtrackCoverageOffsets, parse_coverage_table)
inputCoverages = parse_at_offsets(f, start_tell, inputCoverageOffsets, parse_coverage_table)
lookaheadCoverages = parse_at_offsets(f, start_tell, lookaheadCoverageOffsets, parse_coverage_table)
return ChainContextPosSubtable_Format_3(posFormat, backtrackGlyphCount, backtrackCoverages, inputGlyphCount, inputCoverages, lookaheadGlyphCount, lookaheadCoverages, posCount, posLookupRecords)
case _:
assert False, f"Unimplemented: posFormat: {posFormat}"
assert False, posFormat
case GPOSLookupType.ExtensionPos:
start_tell = f.tell()
posFormat = read_u16(f)
assert posFormat in [1]
match posFormat:
case 1:
2024-05-04 11:34:37 +10:00
extensionLookupType = read_id(f, GPOSLookupType)
2024-05-03 21:02:58 +10:00
assert extensionLookupType != GPOSLookupType.ExtensionPos, f"ExtensionPos subtable cannot reference another ExtensionPos subtable"
extensionOffset = read_u32(f)
with SaveTell(f):
extension = parse_at_offset(f, start_tell, extensionOffset, lambda f: parse_GPOS_lookup_subtable(f, extensionLookupType))
return ExtensionPosSubtable_Format_1(posFormat, extensionLookupType, extension)
case _:
assert False, f"Unimplemented: posFormat: {posFormat}"
assert False, posFormat
case _:
assert False, f"Unimplemented: GPOSLookupType: {lookupType}"
assert False, lookupType
@dataclass
class GPOSTable(Table, ABD):
majorVersion: int
minorVersion: int
scriptList: ScriptListTable
featureList: FeatureListTable
lookupList: LookupListTable[GPOSLookupType, GPOSLookupSubtable]
@dataclass
class GPOSTable_Ver_1_0(GPOSTable): pass
@dataclass
class GPOSTable_Ver_1_1(GPOSTable_Ver_1_0):
featureVariations: Optional[FeatureVariationsTable]
def parse_GPOS_table(f: BinaryIO) -> GPOSTable:
start_tell = f.tell()
majorVersion = read_u16(f)
assert majorVersion == 1
minorVersion = read_u16(f)
assert minorVersion in [0, 1]
scriptListOffset = read_u16(f)
featureListOffset = read_u16(f)
lookupListOffset = read_u16(f)
with SaveTell(f):
scriptList = parse_at_offset(f, start_tell, scriptListOffset, parse_script_list_table)
featureList = parse_at_offset(f, start_tell, featureListOffset, parse_feature_list_table)
lookupList = parse_at_offset(f, start_tell, lookupListOffset, lambda f: parse_lookup_list_table(f, GPOSLookupType, parse_GPOS_lookup_subtable))
if minorVersion == 0:
return GPOSTable_Ver_1_0(majorVersion, minorVersion, scriptList, featureList, lookupList)
featureVariationsOffset = read_u32(f)
with SaveTell(f):
2024-09-15 16:10:41 +10:00
featureVariations = parse_at_offset(f, start_tell, featureVariationsOffset, lambda f: parse_feature_variations_table(f, featureList))
2024-05-03 21:02:58 +10:00
if minorVersion == 1:
return GPOSTable_Ver_1_1(majorVersion, minorVersion, scriptList, featureList, lookupList, featureVariations)
assert False, f"Unimplemented: minorVersion: {minorVersion}"
class GSUBLookupType(Enum):
SingleSubst = 1
MultipleSubst = 2
AlternateSubst = 3
LigatureSubst = 4
ContextSubst = 5
ChainContextSubst = 6
ExtensionSubst = 7
ReverseChainSingleSubst = 8
def __str__(self) -> str: return self._name_
@dataclass
class GSUBLookupSubtable(LookupSubtable, ABD): pass
# 1
@dataclass
class SingleSubstSubtable(GSUBLookupSubtable, ABD):
substFormat: int
@dataclass
class SingleSubstSubtable_Format_1(SingleSubstSubtable):
coverage: CoverageTable
deltaGlyphID: int
@dataclass
class SingleSubstSubtable_Format_2(SingleSubstSubtable):
coverage: CoverageTable
glyphCount: int
substituteGlyphIDs: List[int]
# 2
@dataclass
class MultipleSubstSubtable(GSUBLookupSubtable, ABD):
substFormat: int
@dataclass
class SequenceTable(Table):
glyphCount: int
substituteGlyphIDs: List[int]
def parse_sequence_table(f: BinaryIO) -> SequenceTable:
glyphCount = read_u16(f)
assert glyphCount > 0, "Cannot use multiple substitution to delete an input glyph"
substituteGlyphIDs = [read_u16(f) for _ in range(glyphCount)]
return SequenceTable(glyphCount, substituteGlyphIDs)
@dataclass
class MultipleSubstSubtable_Format_1(MultipleSubstSubtable):
coverage: CoverageTable
sequenceCount: int
sequences: List[SequenceTable]
# 3
@dataclass
class AlternateSubstSubtable(GSUBLookupSubtable, ABD):
substFormat: int
@dataclass
class AlternateSetTable(Table):
glyphCount: int
alternateGlyphIDs: List[int]
def parse_alternate_set_table(f: BinaryIO) -> AlternateSetTable:
glyphCount = read_u16(f)
alternateGlyphIDs = [read_u16(f) for _ in range(glyphCount)]
return AlternateSetTable(glyphCount, alternateGlyphIDs)
@dataclass
class AlternateSubstSubtable_Format_1(AlternateSubstSubtable):
coverage: CoverageTable
alternateSetCount: int
alternateSets: List[AlternateSetTable]
# 4
@dataclass
class SubstLookupRecord:
glyphSequenceIndex: int
lookupListIndex: int
def parse_subst_lookup_record(f: BinaryIO) -> SubstLookupRecord:
glyphSequenceIndex = read_u16(f)
lookupListIndex = read_u16(f)
return SubstLookupRecord(glyphSequenceIndex, lookupListIndex)
@dataclass
class LigatureSubstSubtable(GSUBLookupSubtable, ABD):
substFormat: int
@dataclass
class LigatureTable(Table):
ligatureGlyph: int
componentCount: int
componentGlyphIDs: List[int]
def parse_ligature_table(f: BinaryIO) -> LigatureTable:
ligatureGlyph = read_u16(f)
componentCount = read_u16(f)
componentGlyphIDs = [read_u16(f) for _ in range(componentCount-1)] # starts from second component
return LigatureTable(ligatureGlyph, componentCount, componentGlyphIDs)
2024-05-04 11:34:37 +10:00
LigatureSetTable = SetTable[LigatureTable]
def parse_ligature_set_table(f: BinaryIO) -> LigatureSetTable:
return parse_set_table(f, parse_ligature_table)
2024-05-03 21:02:58 +10:00
@dataclass
class LigatureSubstSubtable_Format_1(LigatureSubstSubtable):
coverage: CoverageTable
ligatureSetCount: int
2024-05-04 11:34:37 +10:00
ligatureSets: List[LigatureSetTable]
2024-05-03 21:02:58 +10:00
# 5
@dataclass
class ContextSubstSubtable(GSUBLookupSubtable, ABD):
substFormat: int
@dataclass
class SubRuleTable(Table):
glyphCount: int
substitutionCount: int
inputSequence: List[int]
substLookupRecords: List[SubstLookupRecord]
def parse_sub_rule_table(f: BinaryIO) -> SubRuleTable:
glyphCount = read_u16(f)
substitutionCount = read_u16(f)
inputSequence = [read_u16(f) for _ in range(glyphCount-1)]
substLookupRecords = [parse_subst_lookup_record(f) for _ in range(substitutionCount)]
return SubRuleTable(glyphCount, substitutionCount, inputSequence, substLookupRecords)
2024-05-04 11:34:37 +10:00
SubRuleSetTable = SetTable[SubRuleTable]
def parse_sub_rule_set_table(f: BinaryIO) -> SubRuleSetTable:
return parse_set_table(f, parse_sub_rule_table)
2024-05-03 21:02:58 +10:00
@dataclass
class ContextSubstSubtable_Format_1(ContextSubstSubtable):
coverage: CoverageTable
subRuleSetCount: int
2024-05-04 11:34:37 +10:00
subRuleSets: List[SubRuleSetTable]
2024-05-03 21:02:58 +10:00
@dataclass
class SubClassRuleTable(Table):
glyphCount: int
substitutionCount: int
inputSequence: List[int]
substLookupRecords: List[SubstLookupRecord]
def parse_sub_class_rule_table(f: BinaryIO) -> SubClassRuleTable:
glyphCount = read_u16(f)
substitutionCount = read_u16(f)
inputSequence = [read_u16(f) for _ in range(glyphCount-1)]
substLookupRecords = [parse_subst_lookup_record(f) for _ in range(substitutionCount)]
return SubClassRuleTable(glyphCount, substitutionCount, inputSequence, substLookupRecords)
2024-05-04 11:34:37 +10:00
SubClassRuleSetTable = SetTable[SubClassRuleTable]
def parse_sub_class_rule_set_table(f: BinaryIO) -> SubClassRuleSetTable:
return parse_set_table(f, parse_sub_class_rule_table)
2024-05-03 21:02:58 +10:00
@dataclass
class ContextSubstSubtable_Format_2(ContextSubstSubtable):
coverage: CoverageTable
classDef: ClassDefTable
subClassSetCount: int
2024-05-04 11:34:37 +10:00
subClassSets: List[Optional[SubClassRuleSetTable]]
2024-05-03 21:02:58 +10:00
@dataclass
class ContextSubstSubtable_Format_3(ContextSubstSubtable):
glyphCount: int
substitutionCount: int
coverages: List[CoverageTable]
substLookupRecords: List[SubstLookupRecord]
# 6
@dataclass
class ChainContextSubstSubtable(GSUBLookupSubtable, ABD):
substFormat: int
@dataclass
class ChainSubRuleTable(Table):
backtrackGlyphCount: int
backtrackSequence: List[int]
inputGlyphCount: int
inputSequence: List[int]
lookaheadGlyphCount: int
lookaheadGlyphSequence: List[int]
substitutionCount: int
substLookupRecords: List[SubstLookupRecord]
def parse_chain_sub_rule_table(f: BinaryIO) -> ChainSubRuleTable:
backtrackGlyphCount = read_u16(f)
backtrackSequence = [read_u16(f) for _ in range(backtrackGlyphCount)]
inputGlyphCount = read_u16(f)
inputSequence = [read_u16(f) for _ in range(inputGlyphCount-1)] # starts from second position, so one less glyph
lookaheadGlyphCount = read_u16(f)
lookAheadSequence = [read_u16(f) for _ in range(lookaheadGlyphCount)]
substitutionCount = read_u16(f)
substLookupRecords = [parse_subst_lookup_record(f) for _ in range(substitutionCount)]
return ChainSubRuleTable(backtrackGlyphCount, backtrackSequence, inputGlyphCount, inputSequence, lookaheadGlyphCount, lookAheadSequence, substitutionCount, substLookupRecords)
2024-05-04 11:34:37 +10:00
ChainSubRuleSetTable = SetTable[ChainSubRuleTable]
def parse_chain_sub_rule_set_table(f: BinaryIO) -> ChainSubRuleSetTable:
return parse_set_table(f, parse_chain_sub_rule_table)
2024-05-03 21:02:58 +10:00
@dataclass
class ChainContextSubstSubtable_Format_1(ChainContextSubstSubtable):
coverage: CoverageTable
chainSubRuleSetCount: int
2024-05-04 11:34:37 +10:00
chainSubRuleSets: List[ChainSubRuleSetTable]
2024-05-03 21:02:58 +10:00
@dataclass
class ChainSubClassRuleTable(Table):
backtrackGlyphCount: int
backtrackSequence: List[int]
inputGlyphCount: int
inputSequence: List[int]
lookaheadGlyphCount: int
lookAheadSequence: List[int]
substitutionCount: int
substLookupRecords: List[SubstLookupRecord]
def parse_chain_sub_class_rule_table(f: BinaryIO) -> ChainSubClassRuleTable:
backtrackGlyphCount = read_u16(f)
backtrackSequence = [read_u16(f) for _ in range(backtrackGlyphCount)]
inputGlyphCount = read_u16(f)
inputSequence = [read_u16(f) for _ in range(inputGlyphCount-1)] # starts from second position, so one less glyph
lookaheadGlyphCount = read_u16(f)
lookAheadSequence = [read_u16(f) for _ in range(lookaheadGlyphCount)]
substitutionCount = read_u16(f)
substLookupRecords = [parse_subst_lookup_record(f) for _ in range(substitutionCount)]
return ChainSubClassRuleTable(backtrackGlyphCount, backtrackSequence, inputGlyphCount, inputSequence, lookaheadGlyphCount, lookAheadSequence, substitutionCount, substLookupRecords)
2024-05-04 11:34:37 +10:00
ChainSubClassRuleSetTable = SetTable[ChainSubClassRuleTable]
def parse_chain_sub_class_rule_set_table(f: BinaryIO) -> ChainSubClassRuleSetTable:
return parse_set_table(f, parse_chain_sub_class_rule_table)
2024-05-03 21:02:58 +10:00
@dataclass
class ChainContextSubstSubtable_Format_2(ChainContextSubstSubtable):
coverage: CoverageTable
backtrackClassDef: ClassDefTable
inputClassDef: ClassDefTable
lookaheadClassDef: ClassDefTable
chainSubClassSetCount: int
2024-05-04 11:34:37 +10:00
chainSubClassSets: List[Optional[ChainSubClassRuleSetTable]]
2024-05-03 21:02:58 +10:00
@dataclass
class ChainContextSubstSubtable_Format_3(ChainContextSubstSubtable):
backtrackGlyphCount: int
backtrackCoverages: List[CoverageTable]
inputGlyphCount: int
inputCoverages: List[CoverageTable]
lookaheadGlyphCount: int
lookaheadCoverages: List[CoverageTable]
substitutionCount: int
substLookupRecords: List[SubstLookupRecord]
# 7
@dataclass
class ExtensionSubstSubtable(GSUBLookupSubtable, ABD):
substFormat: int
@dataclass
class ExtensionSubstSubtable_Format_1(ExtensionSubstSubtable):
extensionLookupType: GSUBLookupType
extension: GSUBLookupSubtable
# 8
@dataclass
class ReverseChainSingleSubstSubtable(GSUBLookupSubtable, ABD):
substFormat: int
@dataclass
class ReverseChainSingleSubstSubtable_Format_1(ReverseChainSingleSubstSubtable):
coverage: CoverageTable
backtrackGlyphCount: int
backtrackCoverages: List[CoverageTable]
lookaheadGlyphCount: int
lookaheadCoverages: List[CoverageTable]
glyphCount: int
substituteGlyphIDs: List[int]
def parse_GSUB_lookup_subtable(f: BinaryIO, lookupType: GSUBLookupType) -> GSUBLookupSubtable:
match lookupType:
case GSUBLookupType.SingleSubst:
start_tell = f.tell()
substFormat = read_u16(f)
assert substFormat in [1, 2]
match substFormat:
case 1:
coverageOffset = read_u16(f)
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
deltaGlyphID = read_i16(f)
return SingleSubstSubtable_Format_1(substFormat, coverage, deltaGlyphID)
case 2:
coverageOffset = read_u16(f)
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
glyphCount = read_u16(f)
substituteGlyphIDs = [read_u16(f) for _ in range(glyphCount)]
return SingleSubstSubtable_Format_2(substFormat, coverage, glyphCount, substituteGlyphIDs)
case _:
assert False, f"Unimplemented: substFormat: {substFormat}"
assert False, substFormat
case GSUBLookupType.MultipleSubst:
start_tell = f.tell()
substFormat = read_u16(f)
assert substFormat in [1]
match substFormat:
case 1:
coverageOffset = read_u16(f)
sequenceCount = read_u16(f)
sequenceOffsets = [read_u16(f) for _ in range(sequenceCount)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
sequences = parse_at_offsets(f, start_tell, sequenceOffsets, parse_sequence_table)
return MultipleSubstSubtable_Format_1(substFormat, coverage, sequenceCount, sequences)
case _:
assert False, f"Unimplemented: substFormat: {substFormat}"
assert False, substFormat
case GSUBLookupType.AlternateSubst:
start_tell = f.tell()
substFormat = read_u16(f)
assert substFormat in [1]
match substFormat:
case 1:
coverageOffset = read_u16(f)
alternateSetCount = read_u16(f)
alternateSetOffsets = [read_u16(f) for _ in range(alternateSetCount)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
alternateSets = parse_at_offsets(f, start_tell, alternateSetOffsets, parse_alternate_set_table)
return AlternateSubstSubtable_Format_1(substFormat, coverage, alternateSetCount, alternateSets)
case _:
assert False, f"Unimplemented: substFormat: {substFormat}"
assert False, substFormat
case GSUBLookupType.LigatureSubst:
start_tell = f.tell()
substFormat = read_u16(f)
assert substFormat in [1]
match substFormat:
case 1:
coverageOffset = read_u16(f)
ligatureSetCount = read_u16(f)
ligatureSetOffsets = [read_u16(f) for _ in range(ligatureSetCount)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
2024-09-15 16:10:41 +10:00
ligatureSets = parse_at_offsets(f, start_tell, ligatureSetOffsets, parse_ligature_set_table)
2024-05-03 21:02:58 +10:00
return LigatureSubstSubtable_Format_1(substFormat, coverage, ligatureSetCount, ligatureSets)
case _:
assert False, f"Unimplemented: substFormat: {substFormat}"
assert False, substFormat
case GSUBLookupType.ContextSubst:
start_tell = f.tell()
substFormat = read_u16(f)
assert substFormat in [1, 2, 3]
match substFormat:
case 1:
coverageOffset = read_u16(f)
subRuleSetCount = read_u16(f)
subRuleSetOffsets = [read_u16(f) for _ in range(subRuleSetCount)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
2024-05-04 11:34:37 +10:00
subRuleSets = parse_at_offsets(f, start_tell, subRuleSetOffsets, parse_sub_rule_set_table)
2024-05-03 21:02:58 +10:00
return ContextSubstSubtable_Format_1(substFormat, coverage, subRuleSetCount, subRuleSets)
case 2:
coverageOffset = read_u16(f)
classDefOffset = read_u16(f)
subClassSetCount = read_u16(f)
subClassSetOffsets = [read_u16(f) for _ in range(subClassSetCount)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
classDef = parse_at_offset(f, start_tell, classDefOffset, parse_class_def_table)
2024-05-04 11:34:37 +10:00
subClassSets = parse_at_optional_offsets(f, start_tell, subClassSetOffsets, parse_sub_class_rule_set_table)
2024-05-03 21:02:58 +10:00
return ContextSubstSubtable_Format_2(substFormat, coverage, classDef, subClassSetCount, subClassSets)
case 3:
glyphCount = read_u16(f)
substitutionCount = read_u16(f)
coverageOffsets = [read_u16(f) for _ in range(glyphCount)]
substLookupRecords = [parse_subst_lookup_record(f) for _ in range(substitutionCount)]
with SaveTell(f):
coverages = parse_at_offsets(f, start_tell, coverageOffsets, parse_coverage_table)
return ContextSubstSubtable_Format_3(substFormat, glyphCount, substitutionCount, coverages, substLookupRecords)
case _:
assert False, f"Unimplemented: substFormat: {substFormat}"
assert False, substFormat
case GSUBLookupType.ChainContextSubst:
start_tell = f.tell()
substFormat = read_u16(f)
assert substFormat in [1, 2, 3]
match substFormat:
case 1:
coverageOffset = read_u16(f)
chainSubRuleSetCount = read_u16(f)
chainSubRuleSetOffsets = [read_u16(f) for _ in range(chainSubRuleSetCount)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
2024-05-04 11:34:37 +10:00
chainSubRuleSets = parse_at_offsets(f, start_tell, chainSubRuleSetOffsets, parse_chain_sub_rule_set_table)
2024-05-03 21:02:58 +10:00
return ChainContextSubstSubtable_Format_1(substFormat, coverage, chainSubRuleSetCount, chainSubRuleSets)
case 2:
coverageOffset = read_u16(f)
backtrackClassDefOffset = read_u16(f)
inputClassDefOffset = read_u16(f)
lookaheadClassDefOffset = read_u16(f)
chainSubClassSetCount = read_u16(f)
chainSubClassSetOffsets = [read_u16(f) for _ in range(chainSubClassSetCount)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
backtrackClassDef = parse_at_offset(f, start_tell, backtrackClassDefOffset, parse_class_def_table)
inputClassDef = parse_at_offset(f, start_tell, inputClassDefOffset, parse_class_def_table)
lookaheadClassDef = parse_at_offset(f, start_tell, lookaheadClassDefOffset, parse_class_def_table)
2024-05-04 11:34:37 +10:00
chainSubClassSets = parse_at_optional_offsets(f, start_tell, chainSubClassSetOffsets, parse_chain_sub_class_rule_set_table)
2024-05-03 21:02:58 +10:00
return ChainContextSubstSubtable_Format_2(substFormat, coverage, backtrackClassDef, inputClassDef, lookaheadClassDef, chainSubClassSetCount, chainSubClassSets)
case 3:
backtrackGlyphCount = read_u16(f)
backtrackCoverageOffsets = [read_u16(f) for _ in range(backtrackGlyphCount)]
inputGlyphCount = read_u16(f)
inputCoverageOffsets = [read_u16(f) for _ in range(inputGlyphCount)]
lookaheadGlyphCount = read_u16(f)
lookaheadCoverageOffsets = [read_u16(f) for _ in range(lookaheadGlyphCount)]
substitutionCount = read_u16(f)
substLookupRecords = [parse_subst_lookup_record(f) for _ in range(substitutionCount)]
with SaveTell(f):
backtrackCoverages = parse_at_offsets(f, start_tell, backtrackCoverageOffsets, parse_coverage_table)
inputCoverages = parse_at_offsets(f, start_tell, inputCoverageOffsets, parse_coverage_table)
lookaheadCoverages = parse_at_offsets(f, start_tell, lookaheadCoverageOffsets, parse_coverage_table)
return ChainContextSubstSubtable_Format_3(substFormat, backtrackGlyphCount, backtrackCoverages, inputGlyphCount, inputCoverages, lookaheadGlyphCount, lookaheadCoverages, substitutionCount, substLookupRecords)
case _:
assert False, f"Unimplemented: substFormat: {substFormat}"
assert False, substFormat
case GSUBLookupType.ExtensionSubst:
start_tell = f.tell()
substFormat = read_u16(f)
assert substFormat in [1]
match substFormat:
case 1:
2024-05-04 11:34:37 +10:00
extensionLookupType = read_id(f, GSUBLookupType)
2024-05-03 21:02:58 +10:00
extensionOffset = read_u32(f)
with SaveTell(f):
extension = parse_at_offset(f, start_tell, extensionOffset, lambda f: parse_GSUB_lookup_subtable(f, extensionLookupType))
return ExtensionSubstSubtable_Format_1(substFormat, extensionLookupType, extension)
case _:
assert False, f"Unimplemented: substFormat: {substFormat}"
assert False, substFormat
case GSUBLookupType.ReverseChainSingleSubst:
start_tell = f.tell()
substFormat = read_u16(f)
assert substFormat in [1]
match substFormat:
case 1:
coverageOffset = read_u16(f)
backtrackGlyphCount = read_u16(f)
backtrackCoverageOffsets = [read_u16(f) for _ in range(backtrackGlyphCount)]
lookaheadGlyphCount = read_u16(f)
lookaheadCoverageOffsets = [read_u16(f) for _ in range(lookaheadGlyphCount)]
glyphCount = read_u16(f)
substituteGlyphIDs = [read_u16(f) for _ in range(glyphCount)]
with SaveTell(f):
coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
backtrackCoverages = parse_at_offsets(f, start_tell, backtrackCoverageOffsets, parse_coverage_table)
lookaheadCoverages = parse_at_offsets(f, start_tell, lookaheadCoverageOffsets, parse_coverage_table)
return ReverseChainSingleSubstSubtable_Format_1(substFormat, coverage, backtrackGlyphCount, backtrackCoverages, lookaheadGlyphCount, lookaheadCoverages, glyphCount, substituteGlyphIDs)
case _:
assert False, f"Unimplemented: substFormat: {substFormat}"
assert False, substFormat
case _:
assert False, f"Unimplemented: GSUBLookupType: {lookupType}"
assert False, lookupType
@dataclass
2024-09-15 16:10:41 +10:00
class GSUBTable(Table, ABD): # TODO: Maybe make a generic class for this, because this is the same as GPOSTable
2024-05-03 21:02:58 +10:00
majorVersion: int
minorVersion: int
# See: https://github.com/MicrosoftDocs/typography-issues/issues/79
scriptList: ScriptListTable
featureList: Optional[FeatureListTable]
lookupList: Optional[LookupListTable[GSUBLookupType, GSUBLookupSubtable]]
@dataclass
class GSUBTable_Ver_1_0(GSUBTable): pass
@dataclass
class GSUBTable_Ver_1_1(GSUBTable_Ver_1_0):
featureVariations: Optional[FeatureVariationsTable]
def parse_GSUB_table(f: BinaryIO) -> GSUBTable:
start_tell = f.tell()
majorVersion = read_u16(f)
assert majorVersion == 1
minorVersion = read_u16(f)
assert minorVersion in [0, 1]
scriptListOffset = read_u16(f)
featureListOffset = read_u16(f)
lookupListOffset = read_u16(f)
with SaveTell(f):
scriptList = parse_at_offset(f, start_tell, scriptListOffset, parse_script_list_table)
featureList = parse_at_optional_offset(f, start_tell, featureListOffset, parse_feature_list_table)
lookupList = parse_at_optional_offset(f, start_tell, lookupListOffset, lambda f: parse_lookup_list_table(f, GSUBLookupType, parse_GSUB_lookup_subtable))
if minorVersion == 0:
return GSUBTable_Ver_1_0(majorVersion, minorVersion, scriptList, featureList, lookupList)
2024-09-15 16:10:41 +10:00
2024-05-03 21:02:58 +10:00
featureVariationsOffset = read_u16(f)
with SaveTell(f):
assert featureList
featureVariations = parse_at_optional_offset(f, start_tell, featureVariationsOffset, lambda f: parse_feature_variations_table(f, featureList))
if minorVersion == 1:
return GSUBTable_Ver_1_1(majorVersion, minorVersion, scriptList, featureList, lookupList, featureVariations)
assert False, f"Unimplemented: minorVersion: {minorVersion}"
@dataclass
class JSTFTable(Table):
pass
def parse_JSTF_table(f: BinaryIO) -> JSTFTable:
assert False
@dataclass
class MATHTable(Table):
pass
def parse_MATH_table(f: BinaryIO) -> MATHTable:
assert False
@dataclass
class AdvancedFeatures:
baseline_data: Optional[BASETable]
glyph_definition_data: Optional[GDEFTable]
glyph_positioning_data: Optional[GPOSTable]
glyph_substitution_data: Optional[GSUBTable]
justification_data: Optional[JSTFTable]
math_layout_data: Optional[MATHTable]
@dataclass
class CvtTable(Table):
fwords: List[int]
def parse_cvt_table(f: BinaryIO, length: int) -> CvtTable:
assert length % 2 == 0, "Must have an integer number of FWORDS in length"
fwords = [read_i16(f) for _ in range(length//2)]
return CvtTable(fwords)
@dataclass
class FpgmTable(Table):
instructions: List[int]
def parse_fpgm_table(f: BinaryIO, length:int) -> FpgmTable:
instructions = [read_u8(f) for _ in range(length)]
return FpgmTable(instructions)
@dataclass
class Glyph(ABD):
numberOfContours: int
xMin: int
yMin: int
xMax: int
yMax: int
@dataclass
class SimpleGlyphFlag:
byte: int
def on_curve_point(self) -> bool: return (self.byte & 0x01)!=0
def x_short(self) -> bool: return (self.byte & 0x02)!=0
def y_short(self) -> bool: return (self.byte & 0x04)!=0
def repeat_flag(self) -> bool: return (self.byte & 0x08)!=0
def x_is_same_or_positive_short(self) -> bool: return (self.byte & 0x10)!=0
def y_is_same_or_positive_short(self) -> bool: return (self.byte & 0x20)!=0
def overlap_simple(self) -> bool: return (self.byte & 0x40)!=0
2024-09-15 16:10:41 +10:00
def __repr__(self) -> str:
return repr_hex(self.byte, 2)
def __post_init__(self):
assert self.byte & 0x80 == 0, self
2024-05-03 21:02:58 +10:00
def parse_simple_glyph_flag(f: BinaryIO) -> SimpleGlyphFlag:
flags = read_u8(f)
2024-09-15 16:10:41 +10:00
2024-05-03 21:02:58 +10:00
return SimpleGlyphFlag(flags)
@dataclass
class SimpleGlyph(Glyph):
endPtsOfContours: List[int]
instructionLength: int
instructions: List[int]
flags: List[SimpleGlyphFlag]
coordinates: List[Tuple[int, int]]
@dataclass
class CompoundGlyphFlag:
2024-09-15 16:10:41 +10:00
bytes: int
2024-05-03 21:02:58 +10:00
2024-09-15 16:10:41 +10:00
def arg_1_and_2_are_words(self) -> bool: return (self.bytes & 0x0001)!=0
def args_are_xy_values(self) -> bool: return (self.bytes & 0x0002)!=0
def round_xy_to_grid(self) -> bool: return (self.bytes & 0x0004)!=0
def we_have_a_scale(self) -> bool: return (self.bytes & 0x0008)!=0
def more_components(self) -> bool: return (self.bytes & 0x0020)!=0
def we_have_an_x_and_y_scale(self) -> bool: return (self.bytes & 0x0040)!=0
def we_have_a_two_by_two(self) -> bool: return (self.bytes & 0x0080)!=0
def we_have_instructions(self) -> bool: return (self.bytes & 0x0100)!=0
def use_my_metrics(self) -> bool: return (self.bytes & 0x0200)!=0
def overlap_compound(self) -> bool: return (self.bytes & 0x0400)!=0
def scaled_component_offset(self) -> bool: return (self.bytes & 0x0800)!=0
def unscaled_component_offset(self) -> bool: return (self.bytes & 0x1000)!=0
2024-05-03 21:02:58 +10:00
def __repr__(self) -> str:
2024-09-15 16:10:41 +10:00
return repr_hex(self.bytes, 4)
def __post_init__(self):
assert self.bytes & 0xE010 == 0, "Reserved"
2024-05-03 21:02:58 +10:00
def parse_compound_glyph_flag(f: BinaryIO) -> CompoundGlyphFlag:
flags = read_u16(f)
2024-09-15 16:10:41 +10:00
2024-05-03 21:02:58 +10:00
return CompoundGlyphFlag(flags)
@dataclass
class Scaling(ABD): pass
@dataclass
class ScaleScaling(Scaling):
scale: float
@dataclass
class XYScaleScaling(Scaling):
xscale: float
yscale: float
@dataclass
class TwoByTwoScaling(Scaling):
xscale: float
scale01: float
scale10: float
yscale: float
@dataclass
class Component:
flag: CompoundGlyphFlag
glyphIndex: int
argument1: int
argument2: int
scaling: Optional[Scaling]
def parse_component(f: BinaryIO) -> Component:
flag = parse_compound_glyph_flag(f)
glyphIndex = read_u16(f)
2024-09-15 16:23:49 +10:00
assert flag.args_are_xy_values(), "TODO: Handle point indexes"
2024-05-03 21:02:58 +10:00
if flag.arg_1_and_2_are_words():
argument1, argument2 = read_i16(f), read_i16(f)
else:
2024-09-15 16:23:49 +10:00
argument1, argument2 = read_i8(f), read_i8(f)
2024-05-03 21:02:58 +10:00
scaling: Optional[Scaling] = None
if flag.we_have_a_scale():
scale = read_F2DOT14(f)
scaling = ScaleScaling(scale)
elif flag.we_have_an_x_and_y_scale():
xscale = read_F2DOT14(f)
yscale = read_F2DOT14(f)
scaling = XYScaleScaling(xscale, yscale)
elif flag.we_have_a_two_by_two():
xscale = read_F2DOT14(f)
scale01 = read_F2DOT14(f)
scale10 = read_F2DOT14(f)
yscale = read_F2DOT14(f)
scaling = TwoByTwoScaling(xscale, scale01, scale10, yscale)
return Component(flag, glyphIndex, argument1, argument2, scaling)
@dataclass
class CompoundGlyph(Glyph):
components: List[Component]
@dataclass
class CompoundGlyphWithInstr(CompoundGlyph):
numInstr: int
instructions: List[int]
def parse_simple_glyph_flags(f: BinaryIO, total_points: int) -> Tuple[List[SimpleGlyphFlag], Tuple[int, int]]:
"""
Returns the logical list (i.e., expanding repeating flags), as well as the total xCoordinates and yCoordinates lengths
"""
# Mostly just ported from https://github.com/RazrFalcon/ttf-parser/blob/2192d3e496201ab2ff39d5437f88d62e70083f0e/src/tables/glyf.rs#L521
x_len = 0
y_len = 0
points_left = total_points
flags: List[SimpleGlyphFlag] = []
while points_left > 0:
flag = parse_simple_glyph_flag(f)
repeats = read_u8(f)+1 if flag.repeat_flag() else 1
for _ in range(repeats): flags.append(flag)
if flag.x_short(): x_len += repeats # 1 byte long
elif not flag.x_is_same_or_positive_short(): x_len += repeats*2 # 2 bytes long
if flag.y_short(): y_len += repeats # 1 byte long
elif not flag.y_is_same_or_positive_short(): y_len += repeats*2 # 2 bytes long
points_left -= repeats
assert points_left == 0
assert len(flags) == total_points
return flags, (x_len, y_len)
def parse_glyph(f: BinaryIO, length: int) -> Glyph:
start_tell = f.tell()
numberOfContours = read_i16(f)
xMin, yMin, xMax, yMax = [read_i16(f) for _ in range(4)]
if numberOfContours >= 0: # simple glyph
endPtsOfContours = [read_u16(f) for _ in range(numberOfContours)]
instructionLength = read_u16(f)
instructions = [read_u8(f) for _ in range(instructionLength)]
total_points = endPtsOfContours[-1]+1
flags, (total_x_len, total_y_len) = parse_simple_glyph_flags(f, total_points)
xCoordinates = BytesIO(f.read(total_x_len))
yCoordinates = BytesIO(f.read(total_y_len))
coordinates: List[Tuple[int, int]] = [(0, 0)]
for flag in flags:
match flag.x_short(), flag.x_is_same_or_positive_short():
case True, sign: xDelta = (2*sign-1)*read_u8(xCoordinates)
case False, True: xDelta = 0
case False, False: xDelta = read_i16(xCoordinates)
match flag.y_short(), flag.y_is_same_or_positive_short():
case True, sign: yDelta = (2*sign-1)*read_u8(yCoordinates)
case False, True: yDelta = 0
case False, False: yDelta = read_i16(yCoordinates)
coordinates.append((coordinates[-1][0]+xDelta, coordinates[-1][1]+yDelta))
2024-09-15 16:10:41 +10:00
# TODO: Do I need to read the padding bytes?
2024-05-03 21:02:58 +10:00
assert length-4<f.tell()-start_tell<=length # there might be padding bytes
2024-09-15 16:10:41 +10:00
assert is_at_end(xCoordinates) and is_at_end(yCoordinates), (len_to_end(xCoordinates), len_to_end(yCoordinates))
2024-05-03 21:02:58 +10:00
return SimpleGlyph(numberOfContours, xMin, yMin, xMax, yMax, endPtsOfContours, instructionLength, instructions, flags, coordinates[1:])
else:
components = [parse_component(f)]
while components[-1].flag.more_components():
components.append(parse_component(f))
if components[-1].flag.we_have_instructions():
numInstr = read_u16(f)
instructions = [read_u8(f) for _ in range(numInstr)]
assert length-4<f.tell()-start_tell<=length # there might be padding bytes
return CompoundGlyphWithInstr(numberOfContours, xMin, yMin, xMax, yMax, components, numInstr, instructions)
assert length-4<f.tell()-start_tell<=length # there might be padding bytes
return CompoundGlyph(numberOfContours, xMin, yMin, xMax, yMax, components)
assert False, numberOfContours
@dataclass
class GlyfTable(Table):
glyphs: List[Optional[Glyph]] # None means no outline
def parse_glyf_table(f: BinaryIO, offsets: List[int]) -> GlyfTable:
start_tell = f.tell()
glyphs = parse_at_offsets_using_length(f, start_tell, offsets, lambda f, _, length: parse_glyph(f, length), zero_is_null=False)
return GlyfTable(glyphs)
@dataclass
class LocaTable(Table):
offsets: List[int]
def parse_loca_table(f: BinaryIO, indexToLocFormat: int, numGlyphs: int) -> LocaTable:
match indexToLocFormat:
case 0: offsets = [read_u16(f)*2 for _ in range(numGlyphs+1)]
case 1: offsets = [read_u32(f) for _ in range(numGlyphs+1)]
case _:
assert False, f"Unimplemented: indexToLocFormat: {indexToLocFormat}"
return LocaTable(offsets)
@dataclass
class PrepTable(Table):
instructions: List[int]
def parse_prep_table(f: BinaryIO, length: int) -> PrepTable:
instructions = [read_u8(f) for _ in range(length)]
return PrepTable(instructions)
@dataclass
class GaspRange:
rangeMaxPPEM: int
rangeGaspBehavior: int
2024-09-15 16:10:41 +10:00
def __post_init__(self):
assert self.rangeGaspBehavior & 0xFFF0 == 0, "Reserved"
def parse_gasp_range(f: BinaryIO, version: int) -> GaspRange:
2024-05-03 21:02:58 +10:00
rangeMaxPPEM = read_u16(f)
rangeGaspBehavior = read_u16(f)
if version == 0: assert rangeGaspBehavior & 0x000C == 0, "Only supported in version 1"
return GaspRange(rangeMaxPPEM, rangeGaspBehavior)
@dataclass
class GaspTable(Table):
version: int
numRanges: int
gaspRanges: List[GaspRange]
def parse_gasp_table(f: BinaryIO) -> GaspTable:
version = read_u16(f)
assert version in [0, 1]
numRanges = read_u16(f)
gaspRanges = [parse_gasp_range(f, version) for _ in range(numRanges)]
return GaspTable(version, numRanges, gaspRanges)
@dataclass
class TrueTypeOutlines:
control_value_table: Optional[CvtTable]
font_program: Optional[FpgmTable]
glyph_data: GlyfTable
2024-09-15 16:10:41 +10:00
index_to_location: LocaTable # Parsing only
2024-05-03 21:02:58 +10:00
CV_program: Optional[PrepTable]
grid_fitting_and_scan_conversion: Optional[GaspTable]
@dataclass
class CompactFontOutlines:
pass
@dataclass
class TupleRecord:
coordinates: List[float]
def parse_tuple_record(f: BinaryIO, axisCount: int) -> TupleRecord:
coordinates = [read_F2DOT14(f) for _ in range(axisCount)]
return TupleRecord(coordinates)
def parse_tuple_record_fixed(f: BinaryIO, axisCount: int) -> TupleRecord:
coordinates = [read_fixed(f) for _ in range(axisCount)]
return TupleRecord(coordinates)
@dataclass
class TupleIndex:
bytes: int
def embedded_peak_tuple(self) -> bool: return (self.bytes & 0x8000)!=0
def intermediate_region(self) -> bool: return (self.bytes & 0x4000)!=0
def private_point_numbers(self) -> bool: return (self.bytes & 0x2000)!=0
def tuple_index(self) -> int: return self.bytes & 0x0fff
2024-09-15 16:10:41 +10:00
def __post_init__(self):
assert self.bytes & 0x1000 == 0, "Reserved"
2024-05-03 21:02:58 +10:00
def parse_tuple_index(f: BinaryIO) -> TupleIndex:
tupleIndex = read_u16(f)
return TupleIndex(tupleIndex)
@dataclass
class TupleVariationHeader:
variationDataSize: int
tupleIndex: TupleIndex
peakTuple: Optional[TupleRecord]
intermediateStartTuple: Optional[TupleRecord]
intermediateEndTuple: Optional[TupleRecord]
def parse_tuple_variation_header(f: BinaryIO, axisCount: int, must_have_peak:bool=False) -> TupleVariationHeader:
variationDataSize = read_u16(f)
tupleIndex = parse_tuple_index(f)
peakTuple = parse_tuple_record(f, axisCount) if tupleIndex.embedded_peak_tuple() else None
if must_have_peak: assert peakTuple, f"Must have a peakTuple"
intermediateStartTuple = parse_tuple_record(f, axisCount) if tupleIndex.intermediate_region() else None
intermediateEndTuple = parse_tuple_record(f, axisCount) if tupleIndex.intermediate_region() else None
return TupleVariationHeader(variationDataSize, tupleIndex, peakTuple, intermediateStartTuple, intermediateEndTuple)
@dataclass
class PackedPointNumbers:
count: Optional[int] # None means that all numbers are included. I.e., All points numbers or all CVT value indices
point_numbers: List[int]
def parse_packed_point_numbers(f: BinaryIO) -> PackedPointNumbers:
POINTS_ARE_WORDS = 0x80
POINT_RUN_COUNT_MASK = 0x7f
first_byte = read_u8(f)
if first_byte == 0:
return PackedPointNumbers(None, [])
elif first_byte & 0x80 != 0:
f.seek(-1, 1)
count = read_u16(f) & 0x7fff # high bit masked out
else:
count = first_byte
curr = 0
point_numbers: List[int] = []
while len(point_numbers) < count:
control_byte = read_u8(f)
run_count = (control_byte & POINT_RUN_COUNT_MASK)+1
for _ in range(run_count):
number_diff = read_u16(f) if (control_byte & POINTS_ARE_WORDS) != 0 else read_u8(f)
curr += number_diff
point_numbers.append(curr)
assert len(point_numbers) == count
return PackedPointNumbers(count, point_numbers)
def parse_packed_deltas(f: BinaryIO, num_deltas: int) -> List[int]:
DELTAS_ARE_ZERO = 0x80
DELTAS_ARE_WORDS = 0x40
DELTA_RUN_COUNT_MASK = 0x3f
def parse_run() -> List[int]:
control_byte = read_u8(f)
count = (control_byte & DELTA_RUN_COUNT_MASK)+1
if (control_byte & DELTAS_ARE_ZERO) != 0:
return [0]*count
elif (control_byte & DELTAS_ARE_WORDS) != 0:
return [read_i16(f) for _ in range(count)]
else:
return [read_i8(f) for _ in range(count)]
deltas: List[int] = []
while len(deltas) < num_deltas: deltas += parse_run()
assert len(deltas) == num_deltas
return deltas
@dataclass
class GvarPerTupleVariationData:
# TODO: Maybe instead of using PackedPointNumbers, use just a List[int] instead?
private_point_numbers: Optional[PackedPointNumbers]
x_coordinate_deltas: List[int]
y_coordinate_deltas: List[int]
def parse_gvar_per_tuple_variation_data(f: BinaryIO, tupleVariationHeader: TupleVariationHeader, shared_point_numbers: Optional[PackedPointNumbers], numPoints: int) -> GvarPerTupleVariationData:
start_tell = f.tell()
private_point_numbers = parse_packed_point_numbers(f) if tupleVariationHeader.tupleIndex.private_point_numbers() else None
point_numbers = private_point_numbers if private_point_numbers else shared_point_numbers
numPoints = point_numbers.count if point_numbers and point_numbers.count is not None else numPoints
deltas = parse_packed_deltas(f, numPoints*2) # because there are two deltas for each point
assert len(deltas)%2 == 0
deltas_count = len(deltas)//2
x_coordinate_deltas = deltas[:deltas_count]
y_coordinate_deltas = deltas[deltas_count:]
assert f.tell()-start_tell == tupleVariationHeader.variationDataSize, (f.tell()-start_tell, tupleVariationHeader)
return GvarPerTupleVariationData(private_point_numbers, x_coordinate_deltas, y_coordinate_deltas)
@dataclass
class CvarPerTupleVariationData:
private_point_numbers: Optional[PackedPointNumbers]
CVT_value_deltas: List[int]
def parse_cvar_per_tuple_variation_data(f: BinaryIO, tupleVariationHeader: TupleVariationHeader, shared_point_numbers: Optional[PackedPointNumbers], numValues: int) -> CvarPerTupleVariationData:
start_tell = f.tell()
private_point_numbers = parse_packed_point_numbers(f) if tupleVariationHeader.tupleIndex.private_point_numbers() else None
point_numbers = private_point_numbers if private_point_numbers else shared_point_numbers
numValues = point_numbers.count if point_numbers and point_numbers.count is not None else numValues
CVT_value_deltas = parse_packed_deltas(f, numValues)
assert f.tell()-start_tell == tupleVariationHeader.variationDataSize, (f.tell()-start_tell, tupleVariationHeader)
return CvarPerTupleVariationData(private_point_numbers, CVT_value_deltas)
2024-09-15 16:10:41 +10:00
def parse_tuple_variation_count(f: BinaryIO) -> Tuple[bool, int]: # TODO: Maybe do this like the other flags?
2024-05-03 21:02:58 +10:00
SHARED_POINT_NUMBERS = 0x8000
COUNT_MASK = 0x0fff
tupleVariationCount = read_u16(f)
assert tupleVariationCount & 0x7000 == 0, "Reserved"
return (tupleVariationCount & SHARED_POINT_NUMBERS) != 0, tupleVariationCount & COUNT_MASK
PerTupleVariationData = TypeVar('PerTupleVariationData', GvarPerTupleVariationData, CvarPerTupleVariationData)
@dataclass
class SerialisedData(Generic[PerTupleVariationData]):
shared_point_numbers: Optional[PackedPointNumbers]
per_tuple_variation_data: List[PerTupleVariationData]
def parse_serialised_data(f: BinaryIO, has_shared_point_numbers: bool, per_tuple_variation_data_parser: Callable[[BinaryIO, TupleVariationHeader, Optional[PackedPointNumbers], int], PerTupleVariationData], tupleVariationHeaders: List[TupleVariationHeader], numPoints: int) -> SerialisedData[PerTupleVariationData]:
shared_point_numbers = parse_packed_point_numbers(f) if has_shared_point_numbers else None
per_tuple_variation_data: List[PerTupleVariationData] = []
for tupleVariationHeader in tupleVariationHeaders:
curr_tell = f.tell()
per_tuple_variation_data.append(per_tuple_variation_data_parser(f, tupleVariationHeader, shared_point_numbers, numPoints))
assert f.tell()-curr_tell == tupleVariationHeader.variationDataSize, (f.tell()-curr_tell, tupleVariationHeader.variationDataSize, tupleVariationHeader.tupleIndex.private_point_numbers())
return SerialisedData(shared_point_numbers, per_tuple_variation_data)
@dataclass
class ItemVariationStoreTable(Table):
format: int
@dataclass
class RegionAxisCoordinates:
startCoord: float
peakCoord: float
endCoord: float
def parse_region_axis_coordinates(f: BinaryIO) -> RegionAxisCoordinates:
startCoord = read_F2DOT14(f)
peakCoord = read_F2DOT14(f)
endCoord = read_F2DOT14(f)
return RegionAxisCoordinates(startCoord, peakCoord, endCoord)
@dataclass
class VariationRegion:
regionAxes: List[RegionAxisCoordinates]
def parse_variation_region(f: BinaryIO, axisCount: int) -> VariationRegion:
regionAxes = [parse_region_axis_coordinates(f) for _ in range(axisCount)]
return VariationRegion(regionAxes)
@dataclass
class VariationRegionList:
axisCount: int
regionCount: int
variationRegions: List[VariationRegion]
def parse_variation_region_list(f: BinaryIO) -> VariationRegionList:
axisCount = read_u16(f)
regionCount = read_u16(f)
variationRegions = [parse_variation_region(f, axisCount) for _ in range(regionCount)]
return VariationRegionList(axisCount, regionCount, variationRegions)
@dataclass
class DeltaSetRecord:
deltaData: List[int]
def parse_delta_set_record(f: BinaryIO, shortDeltaCount: int, regionIndexCount: int) -> DeltaSetRecord:
deltaData = [read_i16(f) for _ in range(shortDeltaCount)] + [read_i8(f) for _ in range(regionIndexCount)]
return DeltaSetRecord(deltaData)
@dataclass
class ItemVariationDataSubtable(Table):
itemCount: int
shortDeltaCount: int
regionIndexCount: int
regionIndexes: List[int]
deltaSets: List[DeltaSetRecord]
def parse_item_variation_data_subtable(f: BinaryIO) -> ItemVariationDataSubtable:
itemCount = read_u16(f)
shortDeltaCount = read_u16(f)
regionIndexCount = read_u16(f)
regionIndexes = [read_u16(f) for _ in range(regionIndexCount)] # ISO is wrong about the number of regionIndexes. They say it's `regionCount`, when it's actualyl `regionIndexCount`
deltaSets = [parse_delta_set_record(f, shortDeltaCount, regionIndexCount) for _ in range(itemCount)]
return ItemVariationDataSubtable(itemCount, shortDeltaCount, regionIndexCount, regionIndexes, deltaSets)
@dataclass
class ItemVariationStoreTable_Format_1(ItemVariationStoreTable):
variationRegionList: VariationRegionList
itemVariationDataCount: int
itemVariationData: List[ItemVariationDataSubtable]
def parse_item_variation_store_table(f: BinaryIO) -> ItemVariationStoreTable:
start_tell = f.tell()
format = read_u16(f)
assert format in [1]
match format:
case 1:
variationRegionListOffset = read_u32(f)
itemVariationDataCount = read_u16(f)
itemVariationDataOffsets = [read_u32(f) for _ in range(itemVariationDataCount)]
with SaveTell(f):
variationRegionList = parse_at_offset(f, start_tell, variationRegionListOffset, parse_variation_region_list)
itemVariationData = parse_at_offsets(f, start_tell, itemVariationDataOffsets, parse_item_variation_data_subtable)
return ItemVariationStoreTable_Format_1(format, variationRegionList, itemVariationDataCount, itemVariationData)
case _:
assert False, f"Unimplemented: format: {format}"
class AxisTag(ABE): pass
class RegisteredAxisTag(AxisTag, Enum):
Italic = 'ital'
OpticalSize = 'opsz'
Slant = 'slnt'
Width = 'wdth'
Weight = 'wght'
is_letter: Callable[[str], bool] = lambda s: 0x41 <= ord(s) <= 0x5a or 0x61 <= ord(s) <= 0x7a
is_digit: Callable[[str], bool] = lambda s: 0x30 <= ord(s) <= 0x39
is_space: Callable[[str], bool] = lambda s: ord(s) == 0x20
is_upper: Callable[[str], bool] = lambda s: 0x41 <= ord(s) <= 0x5a
def is_valid_possible_registed_axis_tag(tag: str) -> bool:
return len(tag) == 4 and is_letter(tag[0]) and all(map(lambda s: is_letter(s) or is_digit(s) or is_space(s), tag[1:])) and len(tag.rstrip(' ')) == len(tag) - tag.count(' ') and not is_valid_possible_foundry_defined_axis_tag(tag)
@dataclass
class FoundryDefinedAxisTag(AxisTag):
tag: str
def is_valid_possible_foundry_defined_axis_tag(tag: str) -> bool:
return len(tag) == 4 and is_upper(tag[0]) and all(map(lambda s: is_upper(s) or is_digit(s), tag[1:]))
def parse_axis_tag(f: BinaryIO) -> AxisTag:
2024-05-04 11:34:37 +10:00
return read_tag_with_conditions(f, (is_valid_possible_registed_axis_tag, RegisteredAxisTag), (is_valid_possible_foundry_defined_axis_tag, FoundryDefinedAxisTag), umbrellaTagCls=AxisTag)
2024-05-03 21:02:58 +10:00
@dataclass
class AxisValueMapRecord:
fromCoordinate: float
toCoordinate: float
def parse_axis_value_map_record(f: BinaryIO) -> AxisValueMapRecord:
fromCoordinate = read_F2DOT14(f)
toCoordinate = read_F2DOT14(f)
return AxisValueMapRecord(fromCoordinate, toCoordinate)
@dataclass
class SegmentMapsRecord:
positionMapCount: int
axisValueMaps: List[AxisValueMapRecord]
def parse_segment_maps_record(f: BinaryIO) -> SegmentMapsRecord:
positionMapCount = read_u16(f)
axisValueMaps = [parse_axis_value_map_record(f) for _ in range(positionMapCount)]
return SegmentMapsRecord(positionMapCount, axisValueMaps)
@dataclass
class AvarTable(Table, ABD):
majorVersion: int
minorVersion: int
axisCount: int
axisSegmentMaps: List[SegmentMapsRecord]
@dataclass
class AvarTable_Ver_1_0(AvarTable): pass
def parse_avar_table(f: BinaryIO) -> AvarTable:
majorVersion = read_u16(f)
assert majorVersion == 1
minorVersion = read_u16(f)
assert minorVersion in [0]
assert read_u16(f) == 0, "Permanently reserved"
axisCount = read_u16(f)
axisSegmentMaps = [parse_segment_maps_record(f) for _ in range(axisCount)]
if minorVersion == 0:
return AvarTable_Ver_1_0(majorVersion, minorVersion, axisCount, axisSegmentMaps)
assert False, f"Unimplemented: minorVersion: {minorVersion}"
@dataclass
class CvarTable(Table, ABD):
majorVersion: int
minorVersion: int
tupleVariationCount: int
data: SerialisedData[CvarPerTupleVariationData]
tupleVariationHeaders: List[TupleVariationHeader]
@dataclass
class CvarTable_Ver_1_0(CvarTable): pass
def parse_cvar_table(f: BinaryIO, axisCount: int, control_value_table: CvtTable) -> CvarTable:
start_tell = f.tell()
majorVersion = read_u16(f)
assert majorVersion == 1
minorVersion = read_u16(f)
assert minorVersion in [0]
has_shared_point_numbers, tupleVariationCount = parse_tuple_variation_count(f)
dataOffset = read_u16(f)
tupleVariationHeaders = [parse_tuple_variation_header(f, axisCount, must_have_peak=True) for _ in range(tupleVariationCount)]
with SaveTell(f):
data = parse_at_offset(f, start_tell, dataOffset, lambda f: parse_serialised_data(f, has_shared_point_numbers, parse_cvar_per_tuple_variation_data, tupleVariationHeaders, len(control_value_table.fwords)))
if minorVersion == 0:
return CvarTable_Ver_1_0(majorVersion, minorVersion, tupleVariationCount, data, tupleVariationHeaders)
assert False, f"Unimplemented: minorVersion: {minorVersion}"
@dataclass
class VariationAxisRecord:
axisTag: AxisTag
minValue: float
defaultValue: float
maxValue: float
flags: int
axisNameID: int
2024-09-15 16:10:41 +10:00
def __post_init__(self):
assert self.flags & 0xfffe == 0, "Reserved"
2024-05-03 21:02:58 +10:00
def parse_variation_axis_record(f: BinaryIO, axisSize: int) -> VariationAxisRecord:
start_tell = f.tell()
axisTag = parse_axis_tag(f)
minValue = read_fixed(f)
defaultValue = read_fixed(f)
maxValue = read_fixed(f)
flags = read_u16(f)
axisNameID = read_u16(f)
assert f.tell() - start_tell == axisSize, (f.tell() - start_tell, axisSize)
return VariationAxisRecord(axisTag, minValue, defaultValue, maxValue, flags, axisNameID)
@dataclass
class InstanceRecord:
subfamilyNameID: int
flags: int
coordinates: TupleRecord
postscriptNameID: Optional[int]
def parse_instance_record(f: BinaryIO, axisCount: int, instanceSize: int) -> InstanceRecord:
start_tell = f.tell()
subfamilyNameID = read_u16(f)
flags = read_u16(f)
assert flags == 0, "Reserved"
coordinates = parse_tuple_record_fixed(f, axisCount)
postscriptNameID: Optional[int] = None
assert f.tell() - start_tell <= instanceSize
if f.tell() - start_tell < instanceSize:
postscriptNameID = read_u16(f)
if postscriptNameID == 0xffff: postscriptNameID = None
assert f.tell() - start_tell == instanceSize, (f.tell() - start_tell, instanceSize)
return InstanceRecord(subfamilyNameID, flags, coordinates, postscriptNameID)
@dataclass
class FvarTable(Table, ABD):
majorVersion: int
minorVersion: int
axisCount: int
axisSize: int
instanceCount: int
instanceSize: int
axes: List[VariationAxisRecord]
instances: List[InstanceRecord]
@dataclass
class FvarTable_Ver_1_0(FvarTable): pass
def parse_fvar_table(f: BinaryIO) -> FvarTable:
start_tell = f.tell()
majorVersion = read_u16(f)
assert majorVersion == 1
minorVersion = read_u16(f)
assert minorVersion in [0]
axesArrayOffset = read_u16(f)
assert axesArrayOffset == 16
assert read_u16(f) == 2, "Reserved"
axisCount = read_u16(f)
axisSize = read_u16(f)
assert axisSize == 20
instanceCount = read_u16(f)
instanceSize = read_u16(f)
with SaveTell(f):
# currently, axesArrayOffset points to where we are now, but if axesArrayOffset changes, this might change.
axes = [parse_variation_axis_record(f, axisSize) for _ in range(axisCount)]
instances = [parse_instance_record(f, axisCount, instanceSize) for _ in range(instanceCount)]
if minorVersion == 0:
return FvarTable_Ver_1_0(majorVersion, minorVersion, axisCount, axisSize, instanceCount, instanceSize, axes, instances)
assert False, f"Unimplemented: minorVersion: {minorVersion}"
@dataclass
class GlyphVariationDataTable(Table):
tupleVariationCount: int
data: SerialisedData[GvarPerTupleVariationData]
tupleVariationHeaders: List[TupleVariationHeader]
PHANTOM_POINTS = 4
def parse_glyph_variation_data_table(f: BinaryIO, axisCount: int, numPoints: int) -> GlyphVariationDataTable:
start_tell = f.tell()
has_shared_point_numbers, tupleVariationCount = parse_tuple_variation_count(f)
dataOffset = read_u16(f)
tupleVariationHeaders = [parse_tuple_variation_header(f, axisCount) for _ in range(tupleVariationCount)]
with SaveTell(f):
data = parse_at_offset(f, start_tell, dataOffset, lambda f: parse_serialised_data(f, has_shared_point_numbers, parse_gvar_per_tuple_variation_data, tupleVariationHeaders, numPoints+PHANTOM_POINTS))
return GlyphVariationDataTable(tupleVariationCount, data, tupleVariationHeaders)
@dataclass
class GvarTable(Table, ABD):
majorVersion: int
minorVersion: int
axisCount: int
sharedTupleCount: int
sharedTuples: List[TupleRecord]
glyphCount: int
flags: int
glyphVariationData: List[Optional[GlyphVariationDataTable]]
2024-09-15 16:10:41 +10:00
def __post_init__(self):
assert self.flags & 0xfffe == 0, "Reserved?" # Maybe not?
2024-05-03 21:02:58 +10:00
@dataclass
class GvarTable_Ver_1_0(GvarTable): pass
def parse_gvar_table(f: BinaryIO, glyph_data: GlyfTable) -> GvarTable:
start_tell = f.tell()
majorVersion = read_u16(f)
assert majorVersion == 1
minorVersion = read_u16(f)
assert minorVersion in [0]
axisCount = read_u16(f)
sharedTupleCount = read_u16(f)
sharedTuplesOffset = read_u32(f)
glyphCount = read_u16(f)
2024-09-15 16:10:41 +10:00
flags = read_u16(f) # TODO: Maybe make a method parse_gvar_flags
2024-05-03 21:02:58 +10:00
long = (flags & 0x0001) != 0
glyphVariationDataArrayOffset = read_u32(f)
glyphVariationDataOffsets = [read_u16(f)*2 for _ in range(glyphCount+1)] if not long else [read_u32(f) for _ in range(glyphCount+1)] # TODO: Some of these point to the same GlyphVariationDataTables. Maybe don't reparse each one if it's the same?
with SaveTell(f):
sharedTuples = parse_list_at_offset(f, start_tell, sharedTuplesOffset, sharedTupleCount, lambda f: parse_tuple_record(f, axisCount))
def get_num_points(glyphID: int) -> int:
match glyph_data.glyphs[glyphID]:
# TODO: Maybe make an issue on ttf-parser because they don't handle the case of there being no outline for gvar data
case None: return 0
case SimpleGlyph(coordinates=coordinates): return len(coordinates)
case CompoundGlyph(components=components): return len(components)
case glyph: assert False, f"Unimplemented: glyph class: {glyph.__class__.__name__}"
glyphVariationData = parse_at_offsets_using_length(f, start_tell+glyphVariationDataArrayOffset, glyphVariationDataOffsets, lambda f, glyphID, _: parse_glyph_variation_data_table(f, axisCount, get_num_points(glyphID)), zero_is_null=False)
if minorVersion == 0:
return GvarTable_Ver_1_0(majorVersion, minorVersion, axisCount, sharedTupleCount, sharedTuples, glyphCount, flags, glyphVariationData)
assert False, f"Unimplemented: minorVersion: {minorVersion}"
@dataclass
class DeltaSetIndexMapTable(Table):
entryFormat: int
mapCount: int
mapData: List[Tuple[int, int]] # (outerIndex, innerIndex)
2024-09-15 16:10:41 +10:00
def __post_init__(self):
assert self.entryFormat & 0xffc0 == 0, "Reserved"
2024-05-03 21:02:58 +10:00
def parse_delta_set_index_map_table(f: BinaryIO) -> DeltaSetIndexMapTable:
INNER_INDEX_BIT_COUNT_MASK = 0x000f
MAP_ENTRY_SIZE_MASK = 0x0030
2024-09-15 16:10:41 +10:00
entryFormat = read_u16(f) # TODO: Maybe make all flags like this? If something is reserved, it could just be future things
2024-05-03 21:02:58 +10:00
map_entry_size = ((entryFormat & MAP_ENTRY_SIZE_MASK) >> 4) + 1
mapCount = read_u16(f)
def parse_entry() -> Tuple[int, int]:
entry = read_int(f, map_entry_size)
return (
entry >> (entryFormat & INNER_INDEX_BIT_COUNT_MASK),
entry & ((1 << (entryFormat & INNER_INDEX_BIT_COUNT_MASK)) - 1)
)
mapData: List[Tuple[int, int]] = [parse_entry() for _ in range(mapCount)]
return DeltaSetIndexMapTable(entryFormat, mapCount, mapData)
@dataclass
class HVARTable(Table, ABD):
majorVersion: int
minorVersion: int
itemVariationStore: ItemVariationStoreTable
advanceWidthMapping: Optional[DeltaSetIndexMapTable]
lsbMapping: Optional[DeltaSetIndexMapTable]
rsbMapping: Optional[DeltaSetIndexMapTable]
@dataclass
class HVARTable_Ver_1_0(HVARTable): pass
def parse_HVAR_table(f: BinaryIO) -> HVARTable:
start_tell = f.tell()
majorVersion = read_u16(f)
assert majorVersion == 1
minorVersion = read_u16(f)
assert minorVersion in [0]
itemVariationStoreOffset = read_u32(f)
advanceWidthMappingOffset = read_u32(f)
lsbMappingOffset = read_u32(f)
rsbMappingOffset = read_u32(f)
with SaveTell(f):
itemVariationStore = parse_at_offset(f, start_tell, itemVariationStoreOffset, parse_item_variation_store_table)
advanceWidthMapping = parse_at_optional_offset(f, start_tell, advanceWidthMappingOffset, parse_delta_set_index_map_table)
lsbMapping = parse_at_optional_offset(f, start_tell, lsbMappingOffset, parse_delta_set_index_map_table)
rsbMapping = parse_at_optional_offset(f, start_tell, rsbMappingOffset, parse_delta_set_index_map_table)
if minorVersion == 0:
return HVARTable_Ver_1_0(majorVersion, minorVersion, itemVariationStore, advanceWidthMapping, lsbMapping, rsbMapping)
assert False, f"Unimplemented: minorVersion: {minorVersion}"
@dataclass
class MVARTable(Table):
pass
def parse_MVAR_table(f: BinaryIO) -> MVARTable:
assert False
@dataclass
class AxisRecord:
axisTag: AxisTag
axisNameID: int
axisOrdering: int
def parse_axis_record(f: BinaryIO, designAxisSize: int) -> AxisRecord:
start_tell = f.tell()
axisTag = parse_axis_tag(f)
axisNameID = read_u16(f)
axisOrdering = read_u16(f)
assert f.tell() - start_tell == designAxisSize, (f.tell() - start_tell, designAxisSize)
return AxisRecord(axisTag, axisNameID, axisOrdering)
@dataclass
class AxisValueTable(Table):
format: int
@dataclass
class AxisValueTableFlags:
bytes: int
def older_sibling_font_attribute(self) -> bool: return (self.bytes & 0x0001)!=0
def elidable_axis_value_name(self) -> bool: return (self.bytes & 0x0002)!=0
def parse_axis_value_table_flags(f: BinaryIO) -> AxisValueTableFlags:
flags = read_u16(f)
# I think bits 2-15 are just empty? They aren't reserved, but there is nothing defined for them.
return AxisValueTableFlags(flags)
@dataclass
class AxisValueTable_Format_1(AxisValueTable):
axisIndex: int
flags: AxisValueTableFlags
valueNameID: int
value: float
@dataclass
class AxisValueTable_Format_2(AxisValueTable):
axisIndex: int
flags: AxisValueTableFlags
valueNameID: int
nominalValue: float
rangeMinValue: float
rangeMaxValue: float
@dataclass
class AxisValueTable_Format_3(AxisValueTable):
axisIndex: int
flags: AxisValueTableFlags
valueNameID: int
value: float
linkedValue: float
def parse_axis_value_table(f: BinaryIO) -> AxisValueTable:
format = read_u16(f)
assert format in [1, 2, 3, 4], f"Invalid format: {format}"
match format:
case 1:
axisIndex = read_u16(f)
flags = parse_axis_value_table_flags(f)
valueNameID = read_u16(f)
value = read_fixed(f)
return AxisValueTable_Format_1(format, axisIndex, flags, valueNameID, value)
case 2:
axisIndex = read_u16(f)
flags = parse_axis_value_table_flags(f)
valueNameID = read_u16(f)
nominalValue = read_fixed(f)
rangeMinValue = read_fixed(f)
rangeMaxValue = read_fixed(f)
return AxisValueTable_Format_2(format, axisIndex, flags, valueNameID, nominalValue, rangeMinValue, rangeMaxValue)
2024-05-03 21:02:58 +10:00
case 3:
axisIndex = read_u16(f)
flags = parse_axis_value_table_flags(f)
valueNameID = read_u16(f)
value = read_fixed(f)
linkedValue = read_fixed(f)
return AxisValueTable_Format_3(format, axisIndex, flags, valueNameID, value, linkedValue)
case _:
assert False, f"Unimplemented: format: {format}"
assert False, format
@dataclass
class STATTable(Table):
majorVersion: int
minorVersion: int
designAxisSize: int
designAxisCount: int
axisValueCount: int
designAxes: List[AxisRecord]
axisValues: List[AxisValueTable]
@dataclass
class STATTable_Ver_1_0(STATTable): pass
@dataclass
class STATTable_Ver_1_1(STATTable_Ver_1_0):
elidedFallbackNameID: int
@dataclass
class STATTable_Ver_1_2(STATTable_Ver_1_1): pass
def parse_STAT_table(f: BinaryIO) -> STATTable:
start_tell = f.tell()
majorVersion = read_u16(f)
assert majorVersion == 1
minorVersion = read_u16(f)
assert minorVersion in [0, 1, 2], f"Invalid minorVersion: {minorVersion}"
designAxisSize = read_u16(f)
designAxisCount = read_u16(f)
offsetToDesignAxes = read_u32(f)
axisValueCount = read_u16(f)
offsetToAxisValueOffsets = read_u32(f)
with SaveTell(f):
designAxes = parse_list_at_offset(f, start_tell, offsetToDesignAxes, designAxisCount, lambda f: parse_axis_record(f, designAxisSize))
axisValueOffsets = parse_list_at_offset(f, start_tell, offsetToAxisValueOffsets, axisValueCount, read_u16)
axisValues = parse_at_offsets(f, start_tell+offsetToAxisValueOffsets, axisValueOffsets, parse_axis_value_table)
if minorVersion == 0:
return STATTable_Ver_1_0(majorVersion, minorVersion, designAxisSize, designAxisCount, axisValueCount, designAxes, axisValues)
elidedFallbackNameID = read_u16(f)
if minorVersion == 1:
return STATTable_Ver_1_1(majorVersion, minorVersion, designAxisSize, designAxisCount, axisValueCount, designAxes, axisValues, elidedFallbackNameID)
if minorVersion == 2:
return STATTable_Ver_1_2(majorVersion, minorVersion, designAxisSize, designAxisCount, axisValueCount, designAxes, axisValues, elidedFallbackNameID)
assert False, minorVersion
@dataclass
class VVARTable(Table):
pass
def parse_VVAR_table(f: BinaryIO) -> VVARTable:
assert False
@dataclass
class FontVariations:
axis_variations: Optional[AvarTable]
CVT_variations: Optional[CvarTable]
font_variations: FvarTable
glyph_variations: Optional[GvarTable]
horizontal_metrics_variations: Optional[HVARTable]
metrics_variations: Optional[MVARTable]
style_attributes: STATTable
vertical_metrics_variations: Optional[VVARTable]
@dataclass
class OpenFontFile:
2024-05-03 21:02:58 +10:00
# required tables
character_to_glyph_mapping: CmapTable
font_header: HeadTable
horizontal_header: HheaTable
horizontal_metrics: HmtxTable
maximum_profile: MaxpTable
naming_table: NameTable
OS2_and_Windows_specific_metrics: OS2Table
PostScript_information: PostTable
# TFF/CFF, SVG
2024-05-03 21:02:58 +10:00
outlines: TrueTypeOutlines | CompactFontOutlines
scalar_vector_graphics: Optional[SvgTable]
2024-05-03 21:02:58 +10:00
# optional tables
digital_signature: Optional[DSIGTable]
horizontal_device_metrics: Optional[HdmxTable]
kerning: Optional[KernTable]
linear_threshold_data: Optional[LTSHTable]
PCL5_data: Optional[PCLTTable]
vertical_device_metrics: Optional[VDMXTable]
vertical_metrics_header: Optional[VheaTable]
vertical_metrics: Optional[VmtxTable]
colour_table: Optional[COLRTable]
colour_palette_table: Optional[CPALTable]
# Other optional things
advanced_features: Optional[AdvancedFeatures]
font_variations: Optional[FontVariations]
# TODO: Maybe have better error reporting if table is missing
def parse_at_table_directory_entry(f: BinaryIO, table: Optional[TableDirectoryEntry], parser: Parser[SomeTable]) -> SomeTable:
assert table, "Table not found"
return parse_at_offset(f, 0, table.offset, parser)
def possibly_parse_at_table_directory_entry(f: BinaryIO, table: Optional[TableDirectoryEntry], parser: Parser[SomeTable]) -> Optional[SomeTable]:
return parse_at_table_directory_entry(f, table, parser) if table else None
def parse_at_table_directory_entry_with_length(f: BinaryIO, table: Optional[TableDirectoryEntry], parser: Callable[[BinaryIO, int], SomeTable]) -> SomeTable:
assert table, "Table not found"
return parse_at_table_directory_entry(f, table, lambda f: parser(f, table.length))
def possibly_parse_at_table_directory_entry_with_length(f: BinaryIO, table: Optional[TableDirectoryEntry], parser: Callable[[BinaryIO, int], SomeTable]) -> Optional[SomeTable]:
return parse_at_table_directory_entry_with_length(f, table, parser) if table else None
def parse_open_font_file(f: BinaryIO) -> OpenFontFile:
2024-05-03 21:02:58 +10:00
font_directory = parse_font_directory(f)
2024-09-15 16:10:41 +10:00
font_header = parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Head), parse_head_table)
horizontal_header = parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Hhea), parse_hhea_table)
maximum_profile = parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Maxp), parse_maxp_table)
horizontal_metrics = parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Hmtx), lambda f: parse_hmtx_table(f, horizontal_header.numberOfHMetrics, maximum_profile.numGlyphs))
naming_table = parse_at_table_directory_entry_with_length(f, font_directory.get_entry(TableTag.Name), parse_name_table)
OS2_and_Windows_specific_metrics = parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.OS2), parse_OS2_table)
character_to_glyph_mapping = parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Cmap), parse_cmap_table)
PostScript_information = parse_at_table_directory_entry_with_length(f, font_directory.get_entry(TableTag.Post), parse_post_table)
2024-05-03 21:02:58 +10:00
# optional
2024-09-15 16:10:41 +10:00
digital_signature = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.DSIG), parse_DSIG_table)
horizontal_device_metrics = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Hdmx), lambda f: parse_hdmx_table(f, maximum_profile.numGlyphs))
kerning = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Kern), parse_Kern_table)
linear_threshold_data = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.LTSH), parse_LTSH_table)
PCL5_data = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.PCLT), parse_PCLT_table)
vertical_device_metrics = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.VDMX), parse_VDMX_table)
vertical_metrics_header = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Vhea), parse_vhea_table)
if font_directory.get_entry(TableTag.Vmtx):
2024-05-03 21:02:58 +10:00
assert vertical_metrics_header, f"Must have vertical_metrics_header to parse vertical_metrics"
2024-09-15 16:10:41 +10:00
vertical_metrics = parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Vmtx), lambda f: parse_vmtx_table(f, vertical_metrics_header.numOfLongVerMetris, maximum_profile.numGlyphs))
2024-05-03 21:02:58 +10:00
else:
vertical_metrics = None
2024-09-15 16:10:41 +10:00
colour_table = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.COLR), parse_COLR_table)
colour_palette_table = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.CPAL), parse_CPAL_table)
2024-05-03 21:02:58 +10:00
# TTF / CFF
match font_directory.offset_table.sfntVersion:
case 0x00010000: # TTF
2024-09-15 16:10:41 +10:00
index_to_location = parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Loca), lambda f: parse_loca_table(f, font_header.indexToLocFormat, maximum_profile.numGlyphs))
glyph_data = parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Glyf), lambda f: parse_glyf_table(f, index_to_location.offsets))
2024-05-03 21:02:58 +10:00
2024-09-15 16:10:41 +10:00
control_value_table = possibly_parse_at_table_directory_entry_with_length(f, font_directory.get_entry(TableTag.Cvt), parse_cvt_table)
font_program = possibly_parse_at_table_directory_entry_with_length(f, font_directory.get_entry(TableTag.Fpgm), parse_fpgm_table)
CV_program = possibly_parse_at_table_directory_entry_with_length(f, font_directory.get_entry(TableTag.Prep), parse_prep_table)
grid_fitting_and_scan_conversion = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Gasp), parse_gasp_table)
2024-05-03 21:02:58 +10:00
outlines = TrueTypeOutlines(control_value_table, font_program, glyph_data, index_to_location, CV_program, grid_fitting_and_scan_conversion)
case _:
assert False, f"Unimplemented: sfntVersion: {hex(font_directory.offset_table.sfntVersion)}"
# SVG
2024-09-15 16:10:41 +10:00
scalar_vector_graphics = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Svg), parse_svg_table)
2024-05-03 21:02:58 +10:00
2024-09-15 16:10:41 +10:00
# Advanced
baseline_data = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.BASE), parse_BASE_table)
glyph_definition_data = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.GDEF), parse_GDEF_table)
glyph_positioning_data = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.GPOS), parse_GPOS_table)
glyph_substitution_data = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.GSUB), parse_GSUB_table)
justification_data = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.JSTF), parse_JSTF_table)
math_layout_data = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.MATH), parse_MATH_table)
2024-05-03 21:02:58 +10:00
advanced_features = AdvancedFeatures(baseline_data, glyph_definition_data, glyph_positioning_data, glyph_substitution_data, justification_data, math_layout_data)
font_variations: Optional[FontVariations] = None
2024-09-15 16:10:41 +10:00
if font_directory.has_entry(TableTag.Fvar):
font_variations_ = parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Fvar), parse_fvar_table)
style_attributes = parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.STAT), parse_STAT_table)
axis_variations = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Avar), parse_avar_table)
if font_directory.has_entry(TableTag.Cvar):
assert isinstance(outlines, TrueTypeOutlines)
cvt = outlines.control_value_table
assert cvt, f"Must have control_value_table in order to have CVT_variations!"
CVT_variations = parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Cvar), lambda f: parse_cvar_table(f, font_variations_.axisCount, cvt))
2024-05-03 21:02:58 +10:00
else:
CVT_variations = None
2024-09-15 16:10:41 +10:00
if font_directory.has_entry(TableTag.Gvar):
2024-05-03 21:02:58 +10:00
assert isinstance(outlines, TrueTypeOutlines)
2024-09-15 16:10:41 +10:00
glyph_variations = parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Gvar), lambda f: parse_gvar_table(f, outlines.glyph_data))
2024-05-03 21:02:58 +10:00
else:
glyph_variations = None
2024-09-15 16:10:41 +10:00
horizontal_metrics_variations = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.HVAR), parse_HVAR_table)
metrics_variations = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.MVAR), parse_MVAR_table)
vertical_metrics_variations = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.VVAR), parse_VVAR_table)
2024-05-03 21:02:58 +10:00
font_variations = FontVariations(axis_variations, CVT_variations, font_variations_, glyph_variations, horizontal_metrics_variations, metrics_variations, style_attributes, vertical_metrics_variations)
2024-09-15 16:10:41 +10:00
return OpenFontFile(character_to_glyph_mapping, font_header, horizontal_header, horizontal_metrics, maximum_profile, naming_table, OS2_and_Windows_specific_metrics, PostScript_information, outlines, scalar_vector_graphics, digital_signature, horizontal_device_metrics, kerning, linear_threshold_data, PCL5_data, vertical_device_metrics, vertical_metrics_header, vertical_metrics, colour_table, colour_palette_table, advanced_features, font_variations)
2024-05-03 21:02:58 +10:00
def open_font_file(file_path: str) -> OpenFontFile: # as in `open (verb) font file (noun)`, not OpenFontFile
2024-05-03 21:02:58 +10:00
with open(file_path, 'rb') as f:
return parse_open_font_file(f)
def get_name(font: OpenFontFile, nameID: NameID) -> str:
assert isinstance(font.naming_table, NameTable_Format_0)
for nameRecord in font.naming_table.nameRecord:
if nameRecord.nameID == nameID:
return nameRecord.string
assert False, f"Name not found: {nameID}"
def get_full_name(font: OpenFontFile) -> str:
return get_name(font, PredefinedNameID.FULL_NAME)
def get_glyph_index(font: OpenFontFile, char: str) -> int:
for encoding_record in font.character_to_glyph_mapping.encodingRecords:
code_point = encoding_record.encodingID.get_code_point(char)
if code_point is not None:
match encoding_record.subtable:
case CmapSubtable_Format_4(
segCountX2=segCountX2,
endCode=end_code,
startCode=start_code,
idDelta=id_delta,
idRangeOffset=id_range_offset,
glyphIdArray=glyph_id_array):
for i, (start, end) in enumerate(zip(start_code, end_code)):
if start <= code_point <= end:
if start == end == 0xFFFF: return 0
if id_range_offset[i] == 0: return (id_delta[i]+code_point)%0xFFFF
index = glyph_id_array[code_point-start+id_range_offset[i]//2+i-segCountX2//2]
if index == 0: return 0
return (index+id_delta[i])%0xFFFF
case CmapSubtable_Format_12(groups=groups):
for group in groups:
if group.startCharCode <= code_point <= group.endCharCode:
assert False, (group, code_point)
case _: assert False, encoding_record.subtable.__class__
return 0
def glyph_count(font: OpenFontFile) -> int:
return font.maximum_profile.numGlyphs