Clean up code
This commit is contained in:
		
							parent
							
								
									adfd9b1ba4
								
							
						
					
					
						commit
						9ed1bfcadb
					
				
							
								
								
									
										109
									
								
								OpenFontFormat/OFF_io_utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								OpenFontFormat/OFF_io_utils.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,109 @@
 | 
			
		||||
 | 
			
		||||
from datetime import datetime, timedelta, timezone
 | 
			
		||||
from types import TracebackType
 | 
			
		||||
from typing import BinaryIO, Callable, List, Optional, Tuple, Type, TypeVar
 | 
			
		||||
 | 
			
		||||
from io_utils import Parser, read_fixed_point, read_u16, read_u64
 | 
			
		||||
 | 
			
		||||
def read_fixed(f: BinaryIO) -> float: # 16.16
 | 
			
		||||
	return read_fixed_point(f, 16, 16)
 | 
			
		||||
 | 
			
		||||
def read_fixed_version(f: BinaryIO) -> float: # Not the same as parse_fixed
 | 
			
		||||
	majorVersion = read_u16(f)
 | 
			
		||||
	minorVersion = read_u16(f)
 | 
			
		||||
	assert minorVersion in [0x0000, 0x1000, 0x5000], f"Invalid fixed minorVersion: {hex(minorVersion)}"
 | 
			
		||||
 | 
			
		||||
	return majorVersion + minorVersion/0xa000 # will need to change if there are ever any versions with 2 decimal digits
 | 
			
		||||
 | 
			
		||||
def read_F2DOT14(f: BinaryIO) -> float: # F2DOT14 (2.14)
 | 
			
		||||
	return read_fixed_point(f, 2, 14)
 | 
			
		||||
 | 
			
		||||
EPOCH = datetime(1904, 1, 1, tzinfo=timezone.utc)
 | 
			
		||||
def read_long_datetime(f: BinaryIO) -> datetime:
 | 
			
		||||
	return EPOCH+timedelta(seconds=read_u64(f))
 | 
			
		||||
 | 
			
		||||
T = TypeVar('T')
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
The following `parse_at_...` functions all modify the tell of f, so it is recommended that you use `SaveTell` to wrap calling these.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
def parse_at_offset(f: BinaryIO, start_tell: int, offset: int, parser: Parser[T], *, zero_is_null:bool=True) -> T:
 | 
			
		||||
	"""
 | 
			
		||||
	Parses a `T` using `parser` at an offset of `offset` from `start_tell`. 
 | 
			
		||||
	
 | 
			
		||||
	If `zero_is_null` is True, then `offset` cannot be 0.
 | 
			
		||||
	Only set `zero_is_null` to False when you are sure that 0 is a valid offset
 | 
			
		||||
	"""
 | 
			
		||||
	if zero_is_null: assert offset != 0, f"Offset was NULL"
 | 
			
		||||
	f.seek(start_tell+offset)
 | 
			
		||||
	return parser(f)
 | 
			
		||||
 | 
			
		||||
def parse_at_optional_offset(f: BinaryIO, start_tell: int, offset: int, parser: Parser[T]) -> Optional[T]:
 | 
			
		||||
	"""
 | 
			
		||||
	Same as `parse_at_offset`, however if the offset is NULL (0), then None is returned.
 | 
			
		||||
 | 
			
		||||
	Should not be used when 0 is a valid offset to something.
 | 
			
		||||
	"""
 | 
			
		||||
	if offset == 0: return None
 | 
			
		||||
	return parse_at_offset(f, start_tell, offset, parser, zero_is_null=True)
 | 
			
		||||
 | 
			
		||||
def parse_at_offsets(f: BinaryIO, start_tell: int, offsets: List[int], parser: Parser[T], *, zero_is_null:bool=True) -> List[T]:
 | 
			
		||||
	return [parse_at_offset(f, start_tell, offset, parser, zero_is_null=zero_is_null) for offset in offsets]
 | 
			
		||||
 | 
			
		||||
def parse_at_optional_offsets(f: BinaryIO, start_tell: int, offsets: List[int], parser: Parser[T]) -> List[Optional[T]]:
 | 
			
		||||
	return [parse_at_optional_offset(f, start_tell, offset, parser) for offset in offsets]
 | 
			
		||||
 | 
			
		||||
def parse_at_offsets_using_length(f: BinaryIO, start_tell: int, offsets: List[int], parser: Callable[[BinaryIO, int, int], T], *, zero_is_null:bool=True) -> List[Optional[T]]:
 | 
			
		||||
	"""
 | 
			
		||||
	The length of the returned list will be one less than that of `offsets`, as the last offset is used to calculate the length of the final element.
 | 
			
		||||
 | 
			
		||||
	`parser` is of the form `(f: BinaryIO, index: int, length: int) -> T`
 | 
			
		||||
	"""
 | 
			
		||||
 | 
			
		||||
	elements: List[Optional[T]] = []
 | 
			
		||||
	for i, offset in enumerate(offsets[:-1]):
 | 
			
		||||
		length = offsets[i+1]-offset
 | 
			
		||||
		if length == 0:
 | 
			
		||||
			elements.append(None)
 | 
			
		||||
			continue
 | 
			
		||||
 | 
			
		||||
		elements.append(parse_at_offset(f, start_tell, offset, lambda f: parser(f, i, length), zero_is_null=zero_is_null))
 | 
			
		||||
 | 
			
		||||
	return elements
 | 
			
		||||
 | 
			
		||||
def parse_list_at_offset(f: BinaryIO, start_tell: int, offset: int, count: int, parser: Parser[T], *, zero_is_null:bool=True) -> List[T]:
 | 
			
		||||
	"""
 | 
			
		||||
	Parses a continuous list of `T`s with `count` elements.
 | 
			
		||||
	"""
 | 
			
		||||
	return parse_at_offset(f, start_tell, offset, lambda f: [parser(f) for _ in range(count)], zero_is_null=zero_is_null)
 | 
			
		||||
 | 
			
		||||
def parse_list_and_use_offsets_into(f: BinaryIO, start_tell: int, offsets: list[int], count: int, parser: Parser[T]) -> Tuple[List[T], List[T]]:
 | 
			
		||||
	elements = [(f.tell(), parser(f)) for _ in range(count)]
 | 
			
		||||
	elements_by_offset_into: List[T] = []
 | 
			
		||||
	for offset in offsets:
 | 
			
		||||
		for (offset_into, element) in elements:
 | 
			
		||||
			if offset + start_tell == offset_into:
 | 
			
		||||
				elements_by_offset_into.append(element)
 | 
			
		||||
				break
 | 
			
		||||
		else:
 | 
			
		||||
			assert False, (f"No element with offset {offset} into this list of elements", start_tell, offsets, elements)
 | 
			
		||||
	return elements_by_offset_into, [element for (_, element) in elements]
 | 
			
		||||
 | 
			
		||||
class SaveTell:
 | 
			
		||||
	"""
 | 
			
		||||
	A context manager that allows operations to be done on a BinaryIO without affecting the tell.
 | 
			
		||||
	"""
 | 
			
		||||
	def __init__(self, f: BinaryIO):
 | 
			
		||||
		self.f = f
 | 
			
		||||
 | 
			
		||||
	def __enter__(self) -> int:
 | 
			
		||||
		self.tell = self.f.tell()
 | 
			
		||||
		return self.tell
 | 
			
		||||
 | 
			
		||||
	def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], exc_tb: Optional[TracebackType]):
 | 
			
		||||
		self.f.seek(self.tell)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def null_if_zero(n: int) -> Optional[int]: return n if n else None
 | 
			
		||||
def nulls_if_zero(ns: List[int]) -> List[Optional[int]]: return list(map(null_if_zero, ns))
 | 
			
		||||
@ -1,29 +1,15 @@
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
from datetime import datetime, timedelta, timezone
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from enum import Enum, EnumMeta
 | 
			
		||||
from io import BytesIO
 | 
			
		||||
from math import floor, log2
 | 
			
		||||
from types import TracebackType
 | 
			
		||||
from typing import  Callable, Generic, List, Optional, Tuple, Type, TypeVar, BinaryIO, Self
 | 
			
		||||
from typing import  Callable, Generic, List, Optional, Tuple, TypeVar, BinaryIO
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
from io_utils import read_ascii, read_fixed_point, read_i16, read_i32, read_i8, read_int, read_pascal_string, read_u16, read_u24, read_u32, read_u64, read_u8
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
def read_fixed(f: BinaryIO) -> float: # 16.16
 | 
			
		||||
	return read_fixed_point(f, 16, 16)
 | 
			
		||||
 | 
			
		||||
def read_fixed_version(f: BinaryIO) -> float: # Not the same as parse_fixed
 | 
			
		||||
	majorVersion = read_u16(f)
 | 
			
		||||
	minorVersion = read_u16(f)
 | 
			
		||||
	assert minorVersion in [0x0000, 0x1000, 0x5000], f"Invalid fixed minorVersion: {hex(minorVersion)}"
 | 
			
		||||
 | 
			
		||||
	return majorVersion + minorVersion/0x1000/10 # will need to change if there are ever any versions with 2 decimal digits
 | 
			
		||||
 | 
			
		||||
def read_F2DOT14(f: BinaryIO) -> float: # F2DOT14 (2.14)
 | 
			
		||||
	return read_fixed_point(f, 2, 14)
 | 
			
		||||
 | 
			
		||||
def read_long_datetime(f: BinaryIO) -> datetime:
 | 
			
		||||
	return datetime(1904, 1, 1, tzinfo=timezone.utc)+timedelta(seconds=read_u64(f))
 | 
			
		||||
T = TypeVar('T')
 | 
			
		||||
 | 
			
		||||
Tag_ = TypeVar('Tag_')
 | 
			
		||||
SomeTag = Callable[[str], Tag_] # If SomeTag is not an EnumMeta, it should throw a ValueError to indicate an invalid tag
 | 
			
		||||
@ -38,11 +24,12 @@ def read_tag_with_conditions(f: BinaryIO, *conditions: Tuple[Callable[[str], boo
 | 
			
		||||
	else:
 | 
			
		||||
		assert False, f"Invalid {umbrellaTagCls.__name__}: '{tag}'"
 | 
			
		||||
 | 
			
		||||
always: Callable[[str], bool] = lambda _: True
 | 
			
		||||
def read_tag_from_tags(f: BinaryIO, *tagClss: SomeTag[Tag_], umbrellaTagCls: type | SomeTag[Tag_], strict:bool=True) -> Tag_:
 | 
			
		||||
	"""
 | 
			
		||||
	This is meant to be used for when some instances of an Enum are just CC01, CC02, CC03, ...
 | 
			
		||||
	"""
 | 
			
		||||
	return read_tag_with_conditions(f, *[(lambda _: True, tagCls) for tagCls in tagClss], umbrellaTagCls=umbrellaTagCls, strict=strict)
 | 
			
		||||
	return read_tag_with_conditions(f, *[(always, tagCls) for tagCls in tagClss], umbrellaTagCls=umbrellaTagCls, strict=strict)
 | 
			
		||||
 | 
			
		||||
def read_tag(f: BinaryIO, tagCls: SomeTag[Tag_], *, strict:bool=True) -> Tag_:
 | 
			
		||||
	return read_tag_from_tags(f, tagCls, umbrellaTagCls=tagCls, strict=strict)
 | 
			
		||||
@ -50,9 +37,6 @@ def read_tag(f: BinaryIO, tagCls: SomeTag[Tag_], *, strict:bool=True) -> Tag_:
 | 
			
		||||
ID_ = TypeVar('ID_')
 | 
			
		||||
SomeID = Callable[[int], ID_]
 | 
			
		||||
 | 
			
		||||
T = TypeVar('T')
 | 
			
		||||
Parser = Callable[[BinaryIO], T]
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	assert len(ranges) > 0, f"Must have at least one range"
 | 
			
		||||
	id = reader(f)
 | 
			
		||||
@ -60,67 +44,11 @@ def read_id_from_ranges(f: BinaryIO, *ranges: Tuple[Optional[int], SomeID[ID_]],
 | 
			
		||||
		if num is not None and id > num: continue
 | 
			
		||||
		try: return idCls(id)
 | 
			
		||||
		except ValueError: pass
 | 
			
		||||
	assert False, f"Invalid {umbrellaIdCls.__name__}: {id}"
 | 
			
		||||
	assert False, f"Invalid {umbrellaIdCls.__name__}: {id} (hex: {repr_hex(id, 2)})"
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
class SaveTell:
 | 
			
		||||
	def __init__(self, f: BinaryIO):
 | 
			
		||||
		self.f = f
 | 
			
		||||
 | 
			
		||||
	def __enter__(self) -> int:
 | 
			
		||||
		self.tell = self.f.tell()
 | 
			
		||||
		return self.tell
 | 
			
		||||
 | 
			
		||||
	def __exit__(self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], exc_tb: Optional[TracebackType]) -> None:
 | 
			
		||||
		self.f.seek(self.tell)
 | 
			
		||||
 | 
			
		||||
# The following `parse_at_...` functions all move the BinaryIO away from wherever it was, so use `with SaveTell(f): ...` to save the tell
 | 
			
		||||
 | 
			
		||||
def parse_at_offset(f: BinaryIO, start_tell: int, offset: int, parser: Parser[T], *, zero_is_null:bool=True) -> T:
 | 
			
		||||
	if zero_is_null: # Some tables have 0 being a valid offset
 | 
			
		||||
		assert offset, f"Offset was NULL"
 | 
			
		||||
	f.seek(start_tell+offset)
 | 
			
		||||
	return parser(f)
 | 
			
		||||
 | 
			
		||||
def parse_at_optional_offset(f: BinaryIO, start_tell: int, offset: Optional[int], parser: Parser[T], *, zero_is_null:bool=True) -> Optional[T]:
 | 
			
		||||
	if zero_is_null:
 | 
			
		||||
		if not offset: return None
 | 
			
		||||
	else:
 | 
			
		||||
		if offset is None: return None
 | 
			
		||||
	return parse_at_offset(f, start_tell, offset, parser, zero_is_null=zero_is_null)
 | 
			
		||||
 | 
			
		||||
def parse_at_offsets(f: BinaryIO, start_tell: int, offsets: List[int], parser: Parser[T], *, zero_is_null:bool=True) -> List[T]:
 | 
			
		||||
	return [parse_at_offset(f, start_tell, offset, parser, zero_is_null=zero_is_null) for offset in offsets]
 | 
			
		||||
 | 
			
		||||
def parse_at_optional_offsets(f: BinaryIO, start_tell: int, offsets: List[int], parser: Parser[T], *, zero_is_null:bool=True) -> List[Optional[T]]:
 | 
			
		||||
	return [parse_at_optional_offset(f, start_tell, offset, parser, zero_is_null=zero_is_null) for offset in offsets]
 | 
			
		||||
 | 
			
		||||
def parse_at_offsets_using_length(f: BinaryIO, start_tell: int, offsets: List[int], parser: Callable[[BinaryIO, int, int], T], *, zero_is_null:bool=True) -> List[Optional[T]]:
 | 
			
		||||
	"""
 | 
			
		||||
	The length of the returned list will be one less than that of `offsets`, as the last offset is used to calculate the length of the final element.
 | 
			
		||||
 | 
			
		||||
	`parser` is of the form `(f: BinaryIO, index: int, length: int) -> T`
 | 
			
		||||
	"""
 | 
			
		||||
 | 
			
		||||
	elements: List[Optional[T]] = []
 | 
			
		||||
	for i, offset in enumerate(offsets[:-1]):
 | 
			
		||||
		length = offsets[i+1]-offset
 | 
			
		||||
		if length == 0:
 | 
			
		||||
			elements.append(None)
 | 
			
		||||
			continue
 | 
			
		||||
 | 
			
		||||
		elements.append(parse_at_offset(f, start_tell, offset, lambda f: parser(f, i, length), zero_is_null=zero_is_null))
 | 
			
		||||
 | 
			
		||||
	return elements
 | 
			
		||||
 | 
			
		||||
def parse_list_at_offset(f: BinaryIO, start_tell: int, offset: int, count: int, parser: Parser[T], *, zero_is_null:bool=True) -> List[T]:
 | 
			
		||||
	return parse_at_offset(f, start_tell, offset, lambda f: [parser(f) for _ in range(count)], zero_is_null=zero_is_null)
 | 
			
		||||
 | 
			
		||||
def null_if_zero(n: int) -> Optional[int]: return n if n else None
 | 
			
		||||
def nulls_if_zero(ns: List[int]) -> List[Optional[int]]: return list(map(null_if_zero, ns))
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class Table(ABD): pass
 | 
			
		||||
 | 
			
		||||
@ -165,9 +93,12 @@ class OffsetTable(Table):
 | 
			
		||||
	entrySelector: int
 | 
			
		||||
	rangeShift: int
 | 
			
		||||
 | 
			
		||||
def repr_hex(value: int, length: int=8) -> str:
 | 
			
		||||
	return f"0x{hex(value)[2:]:0>{length}}"
 | 
			
		||||
 | 
			
		||||
def parse_offset_table(f: BinaryIO) -> OffsetTable:
 | 
			
		||||
	sfntVersion = read_u32(f)
 | 
			
		||||
	assert sfntVersion in [0x00010000, 0x4F54544F], f"Invalid sfntVersion: 0x{hex(sfntVersion)[2:]:0>8}. Expected 0x00010000 or 0x4F54544F."
 | 
			
		||||
	assert sfntVersion in [0x00010000, 0x4F54544F], f"Invalid sfntVersion: {repr_hex(sfntVersion)}. Expected 0x00010000 or 0x4F54544F."
 | 
			
		||||
	numTables = read_u16(f)
 | 
			
		||||
	searchRange = read_u16(f)
 | 
			
		||||
	entrySelector = read_u16(f)
 | 
			
		||||
@ -175,7 +106,7 @@ def parse_offset_table(f: BinaryIO) -> OffsetTable:
 | 
			
		||||
	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
 | 
			
		||||
 | 
			
		||||
	assert rangeShift == numTables*16-searchRange
 | 
			
		||||
	return OffsetTable(sfntVersion, numTables, searchRange, entrySelector, rangeShift)
 | 
			
		||||
 | 
			
		||||
class TableTag(Enum):
 | 
			
		||||
@ -197,7 +128,6 @@ class TableTag(Enum):
 | 
			
		||||
	Prep = 'prep'
 | 
			
		||||
	Gasp = 'gasp' # :O
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	# 5.4 (CFF)
 | 
			
		||||
	...
 | 
			
		||||
 | 
			
		||||
@ -208,7 +138,7 @@ class TableTag(Enum):
 | 
			
		||||
	# 5.6 (Optional)
 | 
			
		||||
	DSIG = 'DSIG'
 | 
			
		||||
	Hdmx = 'hdmx'
 | 
			
		||||
	Kern = 'Kern'
 | 
			
		||||
	Kern = 'kern'
 | 
			
		||||
	LTSH = 'LTSH'
 | 
			
		||||
	PCLT = 'PCLT'
 | 
			
		||||
	VDMX = 'VDMX'
 | 
			
		||||
@ -254,119 +184,25 @@ def parse_table_directory_entry(f: BinaryIO) -> TableDirectoryEntry:
 | 
			
		||||
	length = read_u32(f)
 | 
			
		||||
	return TableDirectoryEntry(tableTag, checkSum, offset, length)
 | 
			
		||||
 | 
			
		||||
# TODO: This should just be a dict[TableTag, TableDirectoryEntry]
 | 
			
		||||
@dataclass
 | 
			
		||||
class FontDirectoryByTable:
 | 
			
		||||
	cmap: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	head: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	hhea: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	hmtx: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	maxp: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	name: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	os2 : Optional[TableDirectoryEntry] = None
 | 
			
		||||
	post: Optional[TableDirectoryEntry] = None
 | 
			
		||||
 | 
			
		||||
	cvt : Optional[TableDirectoryEntry] = None
 | 
			
		||||
	fpgm: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	glyf: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	loca: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	prep: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	gasp: Optional[TableDirectoryEntry] = None
 | 
			
		||||
 | 
			
		||||
	svg : Optional[TableDirectoryEntry] = None
 | 
			
		||||
 | 
			
		||||
	DSIG: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	hdmx: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	Kern: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	LTSH: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	PCLT: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	VDMX: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	vhea: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	vmtx: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	COLR: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	CPAL: Optional[TableDirectoryEntry] = None
 | 
			
		||||
 | 
			
		||||
	BASE: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	GDEF: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	GPOS: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	GSUB: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	JSTF: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	MATH: Optional[TableDirectoryEntry] = None
 | 
			
		||||
 | 
			
		||||
	avar: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	cvar: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	fvar: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	gvar: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	HVAR: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	MVAR: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	STAT: Optional[TableDirectoryEntry] = None
 | 
			
		||||
	VVAR: Optional[TableDirectoryEntry] = None
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class FontDirectory:
 | 
			
		||||
	offset_table: OffsetTable
 | 
			
		||||
	table_directory: List[TableDirectoryEntry]
 | 
			
		||||
 | 
			
		||||
	by_table: FontDirectoryByTable
 | 
			
		||||
	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
 | 
			
		||||
 | 
			
		||||
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)]
 | 
			
		||||
 | 
			
		||||
	by_table = FontDirectoryByTable()
 | 
			
		||||
 | 
			
		||||
	for table_directory_entry in table_directory_entries:
 | 
			
		||||
		match table_directory_entry.tableTag:
 | 
			
		||||
			case TableTag.Cmap: by_table.cmap = table_directory_entry
 | 
			
		||||
			case TableTag.Head: by_table.head = table_directory_entry
 | 
			
		||||
			case TableTag.Hhea: by_table.hhea = table_directory_entry
 | 
			
		||||
			case TableTag.Hmtx: by_table.hmtx = table_directory_entry
 | 
			
		||||
			case TableTag.Maxp: by_table.maxp = table_directory_entry
 | 
			
		||||
			case TableTag.Name: by_table.name = table_directory_entry
 | 
			
		||||
			case TableTag.OS2 : by_table.os2  = table_directory_entry
 | 
			
		||||
			case TableTag.Post: by_table.post = table_directory_entry
 | 
			
		||||
 | 
			
		||||
			case TableTag.Cvt : by_table.cvt  = table_directory_entry
 | 
			
		||||
			case TableTag.Fpgm: by_table.fpgm = table_directory_entry
 | 
			
		||||
			case TableTag.Glyf: by_table.glyf = table_directory_entry
 | 
			
		||||
			case TableTag.Loca: by_table.loca = table_directory_entry
 | 
			
		||||
			case TableTag.Prep: by_table.prep = table_directory_entry
 | 
			
		||||
			case TableTag.Gasp: by_table.gasp = table_directory_entry
 | 
			
		||||
 | 
			
		||||
			case TableTag.Svg : by_table.svg  = table_directory_entry
 | 
			
		||||
 | 
			
		||||
			case TableTag.DSIG: by_table.DSIG = table_directory_entry
 | 
			
		||||
			case TableTag.Hdmx: by_table.hdmx = table_directory_entry
 | 
			
		||||
			case TableTag.Kern: by_table.Kern = table_directory_entry
 | 
			
		||||
			case TableTag.LTSH: by_table.LTSH = table_directory_entry
 | 
			
		||||
			case TableTag.PCLT: by_table.PCLT = table_directory_entry
 | 
			
		||||
			case TableTag.VDMX: by_table.VDMX = table_directory_entry
 | 
			
		||||
			case TableTag.Vhea: by_table.vhea = table_directory_entry
 | 
			
		||||
			case TableTag.Vmtx: by_table.vmtx = table_directory_entry
 | 
			
		||||
			case TableTag.COLR: by_table.COLR = table_directory_entry
 | 
			
		||||
			case TableTag.CPAL: by_table.CPAL = table_directory_entry
 | 
			
		||||
 | 
			
		||||
			case TableTag.BASE: by_table.BASE = table_directory_entry
 | 
			
		||||
			case TableTag.GDEF: by_table.GDEF = table_directory_entry
 | 
			
		||||
			case TableTag.GPOS: by_table.GPOS = table_directory_entry
 | 
			
		||||
			case TableTag.GSUB: by_table.GSUB = table_directory_entry
 | 
			
		||||
			case TableTag.JSTF: by_table.JSTF = table_directory_entry
 | 
			
		||||
			case TableTag.MATH: by_table.MATH = table_directory_entry
 | 
			
		||||
 | 
			
		||||
			case TableTag.Avar: by_table.avar = table_directory_entry
 | 
			
		||||
			case TableTag.Cvar: by_table.cvar = table_directory_entry
 | 
			
		||||
			case TableTag.Fvar: by_table.fvar = table_directory_entry
 | 
			
		||||
			case TableTag.Gvar: by_table.gvar = table_directory_entry
 | 
			
		||||
			case TableTag.HVAR: by_table.HVAR = table_directory_entry
 | 
			
		||||
			case TableTag.MVAR: by_table.MVAR = table_directory_entry
 | 
			
		||||
			case TableTag.STAT: by_table.STAT = table_directory_entry
 | 
			
		||||
			case TableTag.VVAR: by_table.VVAR = table_directory_entry
 | 
			
		||||
 | 
			
		||||
			case _:
 | 
			
		||||
				assert False, f"Unimplemented: tableTag: {table_directory_entry.tableTag}"
 | 
			
		||||
 | 
			
		||||
	return FontDirectory(offset_table, table_directory_entries, by_table)
 | 
			
		||||
 | 
			
		||||
	return FontDirectory(offset_table, table_directory_entries)
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class CmapSubtable(Table, ABD):
 | 
			
		||||
@ -390,7 +226,7 @@ def parse_sub_header(f: BinaryIO) -> SubHeader:
 | 
			
		||||
@dataclass
 | 
			
		||||
class CmapSubtable_Format_2(CmapSubtable):
 | 
			
		||||
	length: int
 | 
			
		||||
	language: int
 | 
			
		||||
	language: int # TODO: Make this an optional int
 | 
			
		||||
	subHeaderKeys: List[int] # 256 elements
 | 
			
		||||
	subHeaders: List[SubHeader]
 | 
			
		||||
	glyphIndexArray: List[int]
 | 
			
		||||
@ -566,7 +402,9 @@ def parse_cmap_subtable(f: BinaryIO, platformID: PlatformID) -> CmapSubtable:
 | 
			
		||||
			entryCount = read_u16(f)
 | 
			
		||||
			glyphIdArray = [read_u16(f) for _ in range(entryCount)]
 | 
			
		||||
 | 
			
		||||
			assert f.tell()-start_tell == length, (f.tell()-start_tell, length)
 | 
			
		||||
			assert length-4<f.tell()-start_tell<=length, (f.tell()-start_tell, length)
 | 
			
		||||
			f.seek(length-(f.tell()-start_tell))
 | 
			
		||||
 | 
			
		||||
			return CmapSubtable_Format_6(format, length, language, firstCode, entryCount, glyphIdArray)
 | 
			
		||||
		case 12:
 | 
			
		||||
			assert read_u16(f) == 0, "Reserved"
 | 
			
		||||
@ -621,7 +459,7 @@ class WindowsEncodingID(EncodingID, Enum):
 | 
			
		||||
	Big5 = 4
 | 
			
		||||
	Wansung = 5
 | 
			
		||||
	Johab = 6
 | 
			
		||||
	UnicodeFull = 10	
 | 
			
		||||
	UnicodeFull = 10
 | 
			
		||||
 | 
			
		||||
	def __str__(self) -> str:
 | 
			
		||||
		return self._name_
 | 
			
		||||
@ -643,6 +481,7 @@ def encoding_ID_cls_from_platform_ID(platformID: PlatformID) -> Callable[[int],
 | 
			
		||||
def parse_encoding_ID(f: BinaryIO, platformID: PlatformID) -> EncodingID:
 | 
			
		||||
	return read_id(f, encoding_ID_cls_from_platform_ID(platformID))
 | 
			
		||||
 | 
			
		||||
# TODO: Finish this
 | 
			
		||||
def parse_string_with_encoding_ID(f: BinaryIO, length: int, encodingID: EncodingID) -> str:
 | 
			
		||||
	bytes = f.read(length)
 | 
			
		||||
	match encodingID:
 | 
			
		||||
@ -687,6 +526,7 @@ def parse_cmap_table(f: BinaryIO) -> CmapTable:
 | 
			
		||||
 | 
			
		||||
	return CmapTable(version, numTables, encodingRecords)
 | 
			
		||||
 | 
			
		||||
HEAD_TABLE_MAGIC = 0x5F0F3CF5
 | 
			
		||||
@dataclass
 | 
			
		||||
class HeadTable(Table):
 | 
			
		||||
	majorVersion: int
 | 
			
		||||
@ -715,7 +555,7 @@ def parse_head_table(f: BinaryIO) -> HeadTable:
 | 
			
		||||
 | 
			
		||||
	fontRevision = read_fixed(f)
 | 
			
		||||
	checkSumAdjustment = read_u32(f)
 | 
			
		||||
	assert read_u32(f) == 0x5F0F3CF5, "magicNumber"
 | 
			
		||||
	assert read_u32(f) == HEAD_TABLE_MAGIC, "magicNumber"
 | 
			
		||||
	flags = read_u16(f)
 | 
			
		||||
	unitsPerEm = read_u16(f)
 | 
			
		||||
	created = read_long_datetime(f)
 | 
			
		||||
@ -867,6 +707,123 @@ class LanguageID(ABE): pass
 | 
			
		||||
 | 
			
		||||
class MacintoshLanguageID(LanguageID, Enum):
 | 
			
		||||
	English = 0
 | 
			
		||||
	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
 | 
			
		||||
 | 
			
		||||
	def __str__(self) -> str: return self._name_
 | 
			
		||||
 | 
			
		||||
@ -1156,7 +1113,7 @@ class NameTable_Format_0(NameTable):
 | 
			
		||||
	count: int
 | 
			
		||||
	nameRecord: List[NameRecord]
 | 
			
		||||
 | 
			
		||||
def parse_name_table(f: BinaryIO, length: int) -> NameTable:
 | 
			
		||||
def parse_name_table(f: BinaryIO, length: int) -> NameTable:	
 | 
			
		||||
	start_tell = f.tell()
 | 
			
		||||
 | 
			
		||||
	format = read_u16(f)
 | 
			
		||||
@ -1175,7 +1132,7 @@ def parse_name_table(f: BinaryIO, length: int) -> NameTable:
 | 
			
		||||
		case _:
 | 
			
		||||
			assert False, f"Unimplemented: format: {format}"
 | 
			
		||||
 | 
			
		||||
	assert False
 | 
			
		||||
	assert False, format
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class VendorTag:
 | 
			
		||||
@ -1218,6 +1175,10 @@ class OS2Table(Table, ABD):
 | 
			
		||||
	usWinAscent: int
 | 
			
		||||
	usWinDescent: int
 | 
			
		||||
 | 
			
		||||
	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"
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class OS2Table_Ver_0(OS2Table): pass
 | 
			
		||||
 | 
			
		||||
@ -1262,9 +1223,6 @@ def parse_OS2_table(f: BinaryIO) -> OS2Table:
 | 
			
		||||
	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 fsSelection & 0x40: assert fsSelection & (0b100001) == 0 # bit 6 indicates that the font is regular, and therefore cannot be bold or italic.
 | 
			
		||||
	assert fsSelection & 0xfc00 == 0, "reserved"
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
@ -1345,6 +1303,8 @@ def parse_post_table(f: BinaryIO, length: int) -> PostTable:
 | 
			
		||||
			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))
 | 
			
		||||
 | 
			
		||||
			assert f.tell()-start_tell == length
 | 
			
		||||
			
 | 
			
		||||
			return PostTable_Ver_2_0(version, italicAngle, underlinePosition, underlineThickness, isFixedPitch, minMemType42, maxMemType42, minMemType1, maxMemType1, numGlyphs, glyphNameIndex, names)
 | 
			
		||||
		case 3.0:
 | 
			
		||||
@ -1414,6 +1374,9 @@ class DSIGTable(Table, ABD):
 | 
			
		||||
	flags: int # It's u16 but only bits 0-7 are documented?
 | 
			
		||||
	signatureRecords: List[SignatureRecord] # there's a typo in the ISO documentation.
 | 
			
		||||
 | 
			
		||||
	def __post_init__(self):
 | 
			
		||||
		assert self.flags & 0xfe == 0, "Reserved"
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class DSIGTable_Ver_1(DSIGTable): pass
 | 
			
		||||
 | 
			
		||||
@ -1425,7 +1388,6 @@ def parse_DSIG_table(f: BinaryIO) -> DSIGTable:
 | 
			
		||||
 | 
			
		||||
	numSignatures = read_u16(f)
 | 
			
		||||
	flags = read_u16(f)
 | 
			
		||||
	assert flags & 0xfe == 0, "Reserved"
 | 
			
		||||
	signatureRecords = [parse_signature_record(f, start_tell) for _ in range(numSignatures)]
 | 
			
		||||
 | 
			
		||||
	if version == 1:
 | 
			
		||||
@ -1678,13 +1640,17 @@ class CCXXTag(ABE):
 | 
			
		||||
		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]}.")
 | 
			
		||||
 | 
			
		||||
	@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
 | 
			
		||||
	# @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
 | 
			
		||||
	
 | 
			
		||||
	def __str__(self) -> str:
 | 
			
		||||
		return f"'{self.__CC__}{self.num:0>2}'"
 | 
			
		||||
	
 | 
			
		||||
	@property
 | 
			
		||||
	def name(self) -> str:
 | 
			
		||||
		return str(self)
 | 
			
		||||
 | 
			
		||||
class ScriptTag(Enum):
 | 
			
		||||
	Adlam = 'adlm'
 | 
			
		||||
@ -2475,8 +2441,12 @@ class ValidLangSysTag(LangSysTag, Enum):
 | 
			
		||||
 | 
			
		||||
	def __str__(self) -> str: return self._name_
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class InvalidLangSysTag(LangSysTag):
 | 
			
		||||
	tag: str
 | 
			
		||||
 | 
			
		||||
def parse_lang_sys_tag(f: BinaryIO) -> LangSysTag:
 | 
			
		||||
	return read_tag_from_tags(f, ValidLangSysTag, MS_VOLT_Tag, umbrellaTagCls=LangSysTag)
 | 
			
		||||
	return read_tag_from_tags(f, ValidLangSysTag, InvalidLangSysTag, MS_VOLT_Tag, umbrellaTagCls=LangSysTag)
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class LangSysRecord:
 | 
			
		||||
@ -2682,8 +2652,18 @@ class MS_VOLT_Tag(CCXXTag, LangSysTag, FeatureTag):
 | 
			
		||||
	__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
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
def parse_feature_tag(f: BinaryIO) -> FeatureTag:
 | 
			
		||||
	return read_tag_from_tags(f, SimpleFeatureTag, CvXXFeatureTag, SsXXFeatureTag, MS_VOLT_Tag, umbrellaTagCls=FeatureTag)
 | 
			
		||||
	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)
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class FeatureParamsTable(Table, ABD): pass
 | 
			
		||||
@ -2828,7 +2808,7 @@ class LookupFlag: # TODO: Do this like the other flags
 | 
			
		||||
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)]
 | 
			
		||||
	assert lookupFlag & 0x00e0 == 0, "Reserved"
 | 
			
		||||
	assert lookupFlag & 0x00e0 == 0, "Reserved" # TODO: Once you do this like the other flags, put this in the __post_init__
 | 
			
		||||
	markAttachmentType = (lookupFlag & 0xff00) >> 8
 | 
			
		||||
 | 
			
		||||
	return LookupFlag(rightToLeft, ignoreBaseGlyphs, ignoreLigatures, ignoreMarks, useMarkFilteringSet, markAttachmentType)
 | 
			
		||||
@ -3091,9 +3071,11 @@ class ValueFormatFlags:
 | 
			
		||||
	def x_advance_device(self) -> bool: 	return (self.bytes & 0x0040)!=0
 | 
			
		||||
	def y_advance_device(self) -> bool: 	return (self.bytes & 0x0080)!=0
 | 
			
		||||
 | 
			
		||||
	def __post_init__(self):
 | 
			
		||||
		assert self.bytes & 0xFF00 == 0, "Reserved"
 | 
			
		||||
 | 
			
		||||
def parse_value_format(f: BinaryIO) -> ValueFormatFlags:
 | 
			
		||||
	valueFormat = read_u16(f)
 | 
			
		||||
	assert valueFormat & 0xFF00 == 0, "Reserved"
 | 
			
		||||
 | 
			
		||||
	return ValueFormatFlags(valueFormat)
 | 
			
		||||
 | 
			
		||||
@ -3104,12 +3086,18 @@ class DeviceTable(Table):
 | 
			
		||||
	deltaFormat: int
 | 
			
		||||
	deltaValue: List[int]
 | 
			
		||||
 | 
			
		||||
	def __post_init__(self):
 | 
			
		||||
		assert self.deltaFormat & 0x7ffc == 0, "Reserved"
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class VariationIndexTable(Table):
 | 
			
		||||
	deltaSetOuterIndex: int
 | 
			
		||||
	deltaSetInnerIndex: int
 | 
			
		||||
	deltaFormat: int
 | 
			
		||||
 | 
			
		||||
	def __post_init__(self):
 | 
			
		||||
		assert self.deltaFormat == 0x8000
 | 
			
		||||
 | 
			
		||||
DeviceTable_ = DeviceTable | VariationIndexTable
 | 
			
		||||
 | 
			
		||||
def parse_device_table(f: BinaryIO) -> DeviceTable_:
 | 
			
		||||
@ -3117,7 +3105,6 @@ def parse_device_table(f: BinaryIO) -> DeviceTable_:
 | 
			
		||||
	second = read_u16(f)
 | 
			
		||||
 | 
			
		||||
	deltaFormat = read_u16(f)
 | 
			
		||||
	assert deltaFormat & 0x7ffc == 0, "Reserved"
 | 
			
		||||
	assert deltaFormat in [1, 2, 3, 0x8000], f"Invalid deltaFormat: {deltaFormat}"
 | 
			
		||||
 | 
			
		||||
	match deltaFormat:
 | 
			
		||||
@ -3476,10 +3463,10 @@ def parse_value_record(f: BinaryIO, start_tell: int, valueFormat: ValueFormatFla
 | 
			
		||||
	xAdvance = read_i16(f) if valueFormat.x_advance() else None
 | 
			
		||||
	yAdvance = read_i16(f) if valueFormat.y_advance() else None
 | 
			
		||||
 | 
			
		||||
	xPlaDeviceOffset = read_u16(f) if valueFormat.x_placement_device() else None
 | 
			
		||||
	yPlaDeviceOffset = read_u16(f) if valueFormat.y_placement_device() else None
 | 
			
		||||
	xAdvDeviceOffset = read_u16(f) if valueFormat.x_advance_device() else None
 | 
			
		||||
	yAdvDeviceOffset = read_u16(f) if valueFormat.y_advance_device() else None
 | 
			
		||||
	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
 | 
			
		||||
	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)
 | 
			
		||||
@ -4222,7 +4209,7 @@ def parse_GPOS_table(f: BinaryIO) -> GPOSTable:
 | 
			
		||||
 | 
			
		||||
	featureVariationsOffset = read_u32(f)
 | 
			
		||||
	with SaveTell(f):
 | 
			
		||||
		featureVariations = parse_at_optional_offset(f, start_tell, featureVariationsOffset, lambda f: parse_feature_variations_table(f, featureList))
 | 
			
		||||
		featureVariations = parse_at_offset(f, start_tell, featureVariationsOffset, lambda f: parse_feature_variations_table(f, featureList))
 | 
			
		||||
	
 | 
			
		||||
	if minorVersion == 1:
 | 
			
		||||
		return GPOSTable_Ver_1_1(majorVersion, minorVersion, scriptList, featureList, lookupList, featureVariations)
 | 
			
		||||
@ -4616,7 +4603,7 @@ def parse_GSUB_lookup_subtable(f: BinaryIO, lookupType: GSUBLookupType) -> GSUBL
 | 
			
		||||
					ligatureSetOffsets = [read_u16(f) for _ in range(ligatureSetCount)]
 | 
			
		||||
					with SaveTell(f):
 | 
			
		||||
						coverage = parse_at_offset(f, start_tell, coverageOffset, parse_coverage_table)
 | 
			
		||||
						ligatureSets = parse_at_offsets(f, start_tell, ligatureSetOffsets, lambda f: parse_set_table(f, parse_ligature_table))
 | 
			
		||||
						ligatureSets = parse_at_offsets(f, start_tell, ligatureSetOffsets, parse_ligature_set_table)
 | 
			
		||||
 | 
			
		||||
					return LigatureSubstSubtable_Format_1(substFormat, coverage, ligatureSetCount, ligatureSets)
 | 
			
		||||
				case _:
 | 
			
		||||
@ -4764,7 +4751,7 @@ def parse_GSUB_lookup_subtable(f: BinaryIO, lookupType: GSUBLookupType) -> GSUBL
 | 
			
		||||
	assert False, lookupType
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class GSUBTable(Table, ABD):
 | 
			
		||||
class GSUBTable(Table, ABD): # TODO: Maybe make a generic class for this, because this is the same as GPOSTable
 | 
			
		||||
	majorVersion: int
 | 
			
		||||
	minorVersion: int
 | 
			
		||||
	# See: https://github.com/MicrosoftDocs/typography-issues/issues/79
 | 
			
		||||
@ -4797,7 +4784,7 @@ def parse_GSUB_table(f: BinaryIO) -> GSUBTable:
 | 
			
		||||
 | 
			
		||||
	if minorVersion == 0:
 | 
			
		||||
		return GSUBTable_Ver_1_0(majorVersion, minorVersion, scriptList, featureList, lookupList)
 | 
			
		||||
	
 | 
			
		||||
 | 
			
		||||
	featureVariationsOffset = read_u16(f)
 | 
			
		||||
	with SaveTell(f):
 | 
			
		||||
		assert featureList
 | 
			
		||||
@ -4869,12 +4856,15 @@ class SimpleGlyphFlag:
 | 
			
		||||
	def y_is_same_or_positive_short(self) -> bool: 	return (self.byte & 0x20)!=0
 | 
			
		||||
	def overlap_simple(self) -> bool: 				return (self.byte & 0x40)!=0
 | 
			
		||||
 | 
			
		||||
	def __str__(self) -> str:
 | 
			
		||||
		return '0x'+hex(self.byte)[2:].rjust(2, '0')
 | 
			
		||||
	def __repr__(self) -> str:
 | 
			
		||||
		return repr_hex(self.byte, 2)
 | 
			
		||||
	
 | 
			
		||||
	def __post_init__(self):
 | 
			
		||||
		assert self.byte & 0x80 == 0, self
 | 
			
		||||
 | 
			
		||||
def parse_simple_glyph_flag(f: BinaryIO) -> SimpleGlyphFlag:
 | 
			
		||||
	flags = read_u8(f)
 | 
			
		||||
	assert flags & 0x80 == 0, "reserved"
 | 
			
		||||
 | 
			
		||||
	return SimpleGlyphFlag(flags)
 | 
			
		||||
	
 | 
			
		||||
@dataclass
 | 
			
		||||
@ -4887,27 +4877,30 @@ class SimpleGlyph(Glyph):
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class CompoundGlyphFlag:
 | 
			
		||||
	byte: int
 | 
			
		||||
	bytes: int
 | 
			
		||||
 | 
			
		||||
	def arg_1_and_2_are_words(self) -> bool: 		return (self.byte & 0x0001)!=0
 | 
			
		||||
	def args_are_xy_values(self) -> bool: 			return (self.byte & 0x0002)!=0
 | 
			
		||||
	def round_xy_to_grid(self) -> bool: 			return (self.byte & 0x0004)!=0
 | 
			
		||||
	def we_have_a_scale(self) -> bool: 				return (self.byte & 0x0008)!=0
 | 
			
		||||
	def more_components(self) -> bool: 				return (self.byte & 0x0020)!=0
 | 
			
		||||
	def we_have_an_x_and_y_scale(self) -> bool: 	return (self.byte & 0x0040)!=0
 | 
			
		||||
	def we_have_a_two_by_two(self) -> bool: 		return (self.byte & 0x0080)!=0
 | 
			
		||||
	def we_have_instructions(self) -> bool: 		return (self.byte & 0x0100)!=0
 | 
			
		||||
	def use_my_metrics(self) -> bool: 				return (self.byte & 0x0200)!=0
 | 
			
		||||
	def overlap_compound(self) -> bool: 			return (self.byte & 0x0400)!=0
 | 
			
		||||
	def scaled_component_offset(self) -> bool: 		return (self.byte & 0x0800)!=0
 | 
			
		||||
	def unscaled_component_offset(self) -> bool: 	return (self.byte & 0x1000)!=0
 | 
			
		||||
	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
 | 
			
		||||
 | 
			
		||||
	def __repr__(self) -> str:
 | 
			
		||||
		return '0x'+hex(self.byte)[2:].rjust(4, '0')
 | 
			
		||||
		return repr_hex(self.bytes, 4)
 | 
			
		||||
	
 | 
			
		||||
	def __post_init__(self):
 | 
			
		||||
		assert self.bytes & 0xE010 == 0, "Reserved"
 | 
			
		||||
 | 
			
		||||
def parse_compound_glyph_flag(f: BinaryIO) -> CompoundGlyphFlag:
 | 
			
		||||
	flags = read_u16(f)
 | 
			
		||||
	assert flags & 0xE010 == 0, "reserved"
 | 
			
		||||
 | 
			
		||||
	return CompoundGlyphFlag(flags)
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
@ -4977,7 +4970,6 @@ def parse_simple_glyph_flags(f: BinaryIO, total_points: int) -> Tuple[List[Simpl
 | 
			
		||||
	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
 | 
			
		||||
 | 
			
		||||
@ -5033,8 +5025,9 @@ def parse_glyph(f: BinaryIO, length: int) -> Glyph:
 | 
			
		||||
				case False, False: yDelta = read_i16(yCoordinates)
 | 
			
		||||
 | 
			
		||||
			coordinates.append((coordinates[-1][0]+xDelta, coordinates[-1][1]+yDelta))
 | 
			
		||||
 | 
			
		||||
		# TODO: Do I need to read the padding bytes?
 | 
			
		||||
		assert length-4<f.tell()-start_tell<=length # there might be padding bytes
 | 
			
		||||
		assert is_at_end(xCoordinates) and is_at_end(yCoordinates), (len_to_end(xCoordinates), len_to_end(yCoordinates))
 | 
			
		||||
		return SimpleGlyph(numberOfContours, xMin, yMin, xMax, yMax, endPtsOfContours, instructionLength, instructions, flags, coordinates[1:])
 | 
			
		||||
	else:
 | 
			
		||||
		components = [parse_component(f)]
 | 
			
		||||
@ -5091,12 +5084,14 @@ class GaspRange:
 | 
			
		||||
	rangeMaxPPEM: int
 | 
			
		||||
	rangeGaspBehavior: int
 | 
			
		||||
 | 
			
		||||
def parse_gasp_range(f: BinaryIO, version:int) -> GaspRange:
 | 
			
		||||
	def __post_init__(self):
 | 
			
		||||
		assert self.rangeGaspBehavior & 0xFFF0 == 0, "Reserved"
 | 
			
		||||
 | 
			
		||||
def parse_gasp_range(f: BinaryIO, version: int) -> GaspRange:
 | 
			
		||||
	rangeMaxPPEM = read_u16(f)
 | 
			
		||||
	rangeGaspBehavior = read_u16(f)
 | 
			
		||||
 | 
			
		||||
	if version == 0: assert rangeGaspBehavior & 0x000C == 0, "Only supported in version 1"
 | 
			
		||||
	assert rangeGaspBehavior & 0xFFF0 == 0, "Reserved"
 | 
			
		||||
 | 
			
		||||
	return GaspRange(rangeMaxPPEM, rangeGaspBehavior)
 | 
			
		||||
 | 
			
		||||
@ -5120,7 +5115,7 @@ class TrueTypeOutlines:
 | 
			
		||||
	control_value_table: Optional[CvtTable]
 | 
			
		||||
	font_program: Optional[FpgmTable]	
 | 
			
		||||
	glyph_data: GlyfTable
 | 
			
		||||
	index_to_location: LocaTable
 | 
			
		||||
	index_to_location: LocaTable # Parsing only
 | 
			
		||||
	CV_program: Optional[PrepTable]
 | 
			
		||||
	grid_fitting_and_scan_conversion: Optional[GaspTable]
 | 
			
		||||
 | 
			
		||||
@ -5152,9 +5147,11 @@ class TupleIndex:
 | 
			
		||||
 | 
			
		||||
	def tuple_index(self) -> int: return self.bytes & 0x0fff
 | 
			
		||||
 | 
			
		||||
	def __post_init__(self):
 | 
			
		||||
		assert self.bytes & 0x1000 == 0, "Reserved"
 | 
			
		||||
 | 
			
		||||
def parse_tuple_index(f: BinaryIO) -> TupleIndex:
 | 
			
		||||
	tupleIndex = read_u16(f)
 | 
			
		||||
	assert tupleIndex & 0x1000 == 0, "Reserved"
 | 
			
		||||
 | 
			
		||||
	return TupleIndex(tupleIndex)
 | 
			
		||||
 | 
			
		||||
@ -5269,7 +5266,7 @@ def parse_cvar_per_tuple_variation_data(f: BinaryIO, tupleVariationHeader: Tuple
 | 
			
		||||
	assert f.tell()-start_tell == tupleVariationHeader.variationDataSize, (f.tell()-start_tell, tupleVariationHeader)
 | 
			
		||||
	return CvarPerTupleVariationData(private_point_numbers, CVT_value_deltas)
 | 
			
		||||
 | 
			
		||||
def parse_tuple_variation_count(f: BinaryIO) -> Tuple[bool, int]:
 | 
			
		||||
def parse_tuple_variation_count(f: BinaryIO) -> Tuple[bool, int]: # TODO: Maybe do this like the other flags?
 | 
			
		||||
	SHARED_POINT_NUMBERS = 0x8000
 | 
			
		||||
	COUNT_MASK = 0x0fff
 | 
			
		||||
 | 
			
		||||
@ -5498,6 +5495,9 @@ class VariationAxisRecord:
 | 
			
		||||
	flags: int
 | 
			
		||||
	axisNameID: int
 | 
			
		||||
 | 
			
		||||
	def __post_init__(self):
 | 
			
		||||
		assert self.flags & 0xfffe == 0, "Reserved"
 | 
			
		||||
 | 
			
		||||
def parse_variation_axis_record(f: BinaryIO, axisSize: int) -> VariationAxisRecord:
 | 
			
		||||
	start_tell = f.tell()
 | 
			
		||||
 | 
			
		||||
@ -5506,7 +5506,6 @@ def parse_variation_axis_record(f: BinaryIO, axisSize: int) -> VariationAxisReco
 | 
			
		||||
	defaultValue = read_fixed(f)
 | 
			
		||||
	maxValue = read_fixed(f)
 | 
			
		||||
	flags = read_u16(f)
 | 
			
		||||
	assert flags & 0xfffe == 0, "Reserved"
 | 
			
		||||
	axisNameID = read_u16(f)
 | 
			
		||||
 | 
			
		||||
	assert f.tell() - start_tell == axisSize, (f.tell() - start_tell, axisSize)
 | 
			
		||||
@ -5608,6 +5607,9 @@ class GvarTable(Table, ABD):
 | 
			
		||||
	flags: int
 | 
			
		||||
	glyphVariationData: List[Optional[GlyphVariationDataTable]]
 | 
			
		||||
 | 
			
		||||
	def __post_init__(self):
 | 
			
		||||
		assert self.flags & 0xfffe == 0, "Reserved?" # Maybe not?
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class GvarTable_Ver_1_0(GvarTable): pass
 | 
			
		||||
 | 
			
		||||
@ -5623,8 +5625,7 @@ def parse_gvar_table(f: BinaryIO, glyph_data: GlyfTable) -> GvarTable:
 | 
			
		||||
	sharedTupleCount = read_u16(f)
 | 
			
		||||
	sharedTuplesOffset = read_u32(f)
 | 
			
		||||
	glyphCount = read_u16(f)
 | 
			
		||||
	flags = read_u16(f)
 | 
			
		||||
	assert flags & 0xfffe == 0, f"Reserved?" # Maybe not?
 | 
			
		||||
	flags = read_u16(f) # TODO: Maybe make a method parse_gvar_flags
 | 
			
		||||
	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?
 | 
			
		||||
@ -5652,12 +5653,14 @@ class DeltaSetIndexMapTable(Table):
 | 
			
		||||
	mapCount: int
 | 
			
		||||
	mapData: List[Tuple[int, int]] # (outerIndex, innerIndex)
 | 
			
		||||
 | 
			
		||||
	def __post_init__(self):
 | 
			
		||||
		assert self.entryFormat & 0xffc0 == 0, "Reserved"
 | 
			
		||||
 | 
			
		||||
def parse_delta_set_index_map_table(f: BinaryIO) -> DeltaSetIndexMapTable:
 | 
			
		||||
	INNER_INDEX_BIT_COUNT_MASK = 0x000f
 | 
			
		||||
	MAP_ENTRY_SIZE_MASK = 0x0030
 | 
			
		||||
 | 
			
		||||
	entryFormat = read_u16(f)
 | 
			
		||||
	assert entryFormat & 0xffc0 == 0, "Reserved"
 | 
			
		||||
	entryFormat = read_u16(f) # TODO: Maybe make all flags like this? If something is reserved, it could just be future things
 | 
			
		||||
	map_entry_size = ((entryFormat & MAP_ENTRY_SIZE_MASK) >> 4) + 1
 | 
			
		||||
 | 
			
		||||
	mapCount = read_u16(f)
 | 
			
		||||
@ -5917,81 +5920,83 @@ def possibly_parse_at_table_directory_entry_with_length(f: BinaryIO, table: Opti
 | 
			
		||||
def parse_open_font_file(f: BinaryIO) -> OpenFontFile:
 | 
			
		||||
	font_directory = parse_font_directory(f)
 | 
			
		||||
 | 
			
		||||
	font_header = parse_at_table_directory_entry(f, font_directory.by_table.head, parse_head_table)
 | 
			
		||||
	horizontal_header = parse_at_table_directory_entry(f, font_directory.by_table.hhea, parse_hhea_table)
 | 
			
		||||
	maximum_profile = parse_at_table_directory_entry(f, font_directory.by_table.maxp, parse_maxp_table)
 | 
			
		||||
	horizontal_metrics = parse_at_table_directory_entry(f, font_directory.by_table.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.by_table.name, parse_name_table)
 | 
			
		||||
	OS2_and_Windows_specific_metrics = parse_at_table_directory_entry(f, font_directory.by_table.os2, parse_OS2_table)
 | 
			
		||||
	character_to_glyph_mapping = parse_at_table_directory_entry(f, font_directory.by_table.cmap, parse_cmap_table)
 | 
			
		||||
	PostScript_information = parse_at_table_directory_entry_with_length(f, font_directory.by_table.post, parse_post_table)
 | 
			
		||||
	
 | 
			
		||||
	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)
 | 
			
		||||
 | 
			
		||||
	# optional
 | 
			
		||||
	digital_signature = possibly_parse_at_table_directory_entry(f, font_directory.by_table.DSIG, parse_DSIG_table)
 | 
			
		||||
	horizontal_device_metrics = possibly_parse_at_table_directory_entry(f, font_directory.by_table.hdmx, lambda f: parse_hdmx_table(f, maximum_profile.numGlyphs))
 | 
			
		||||
	kerning = possibly_parse_at_table_directory_entry(f, font_directory.by_table.Kern, parse_Kern_table)
 | 
			
		||||
	linear_threshold_data = possibly_parse_at_table_directory_entry(f, font_directory.by_table.LTSH, parse_LTSH_table)
 | 
			
		||||
	PCL5_data = possibly_parse_at_table_directory_entry(f, font_directory.by_table.PCLT, parse_PCLT_table)
 | 
			
		||||
	vertical_device_metrics = possibly_parse_at_table_directory_entry(f, font_directory.by_table.VDMX, parse_VDMX_table)
 | 
			
		||||
	vertical_metrics_header = possibly_parse_at_table_directory_entry(f, font_directory.by_table.vhea, parse_vhea_table)
 | 
			
		||||
	if font_directory.by_table.vmtx:
 | 
			
		||||
	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):
 | 
			
		||||
		assert vertical_metrics_header, f"Must have vertical_metrics_header to parse vertical_metrics"
 | 
			
		||||
		vertical_metrics = parse_at_table_directory_entry(f, font_directory.by_table.vmtx, lambda f: parse_vmtx_table(f, vertical_metrics_header.numOfLongVerMetris, maximum_profile.numGlyphs))
 | 
			
		||||
		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))
 | 
			
		||||
	else:
 | 
			
		||||
		vertical_metrics = None
 | 
			
		||||
	colour_table = possibly_parse_at_table_directory_entry(f, font_directory.by_table.COLR, parse_COLR_table)
 | 
			
		||||
	colour_palette_table = possibly_parse_at_table_directory_entry(f, font_directory.by_table.CPAL, parse_CPAL_table)
 | 
			
		||||
	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)
 | 
			
		||||
 | 
			
		||||
	# TTF / CFF
 | 
			
		||||
	match font_directory.offset_table.sfntVersion:
 | 
			
		||||
		case 0x00010000: # TTF
 | 
			
		||||
			index_to_location = parse_at_table_directory_entry(f, font_directory.by_table.loca, lambda f: parse_loca_table(f, font_header.indexToLocFormat, maximum_profile.numGlyphs))
 | 
			
		||||
			glyph_data = parse_at_table_directory_entry(f, font_directory.by_table.glyf, lambda f: parse_glyf_table(f, index_to_location.offsets))
 | 
			
		||||
			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))
 | 
			
		||||
 | 
			
		||||
			control_value_table = possibly_parse_at_table_directory_entry_with_length(f, font_directory.by_table.cvt, parse_cvt_table)
 | 
			
		||||
			font_program = possibly_parse_at_table_directory_entry_with_length(f, font_directory.by_table.fpgm, parse_fpgm_table)
 | 
			
		||||
			CV_program = possibly_parse_at_table_directory_entry_with_length(f, font_directory.by_table.prep, parse_prep_table)
 | 
			
		||||
			grid_fitting_and_scan_conversion = possibly_parse_at_table_directory_entry(f, font_directory.by_table.gasp, parse_gasp_table)
 | 
			
		||||
			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)
 | 
			
		||||
			
 | 
			
		||||
			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
 | 
			
		||||
	scalar_vector_graphics = possibly_parse_at_table_directory_entry(f, font_directory.by_table.svg, parse_svg_table)
 | 
			
		||||
	scalar_vector_graphics = possibly_parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Svg), parse_svg_table)
 | 
			
		||||
 | 
			
		||||
	# Advanced 
 | 
			
		||||
	baseline_data = possibly_parse_at_table_directory_entry(f, font_directory.by_table.BASE, parse_BASE_table)
 | 
			
		||||
	glyph_definition_data = possibly_parse_at_table_directory_entry(f, font_directory.by_table.GDEF, parse_GDEF_table)
 | 
			
		||||
	glyph_positioning_data = possibly_parse_at_table_directory_entry(f, font_directory.by_table.GPOS, parse_GPOS_table)
 | 
			
		||||
	glyph_substitution_data = possibly_parse_at_table_directory_entry(f, font_directory.by_table.GSUB, parse_GSUB_table)
 | 
			
		||||
	justification_data = possibly_parse_at_table_directory_entry(f, font_directory.by_table.JSTF, parse_JSTF_table)
 | 
			
		||||
	math_layout_data = possibly_parse_at_table_directory_entry(f, font_directory.by_table.MATH, parse_MATH_table)
 | 
			
		||||
	# 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)
 | 
			
		||||
 | 
			
		||||
	advanced_features = AdvancedFeatures(baseline_data, glyph_definition_data, glyph_positioning_data, glyph_substitution_data, justification_data, math_layout_data)
 | 
			
		||||
 | 
			
		||||
	font_variations: Optional[FontVariations] = None
 | 
			
		||||
	if font_directory.by_table.fvar:
 | 
			
		||||
		font_variations_ = parse_at_table_directory_entry(f, font_directory.by_table.fvar, parse_fvar_table)
 | 
			
		||||
		style_attributes = parse_at_table_directory_entry(f, font_directory.by_table.STAT, parse_STAT_table)
 | 
			
		||||
	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.by_table.avar, parse_avar_table)
 | 
			
		||||
		if font_directory.by_table.cvar:
 | 
			
		||||
			assert control_value_table, f"Must have control_value_table in order to have CVT_variations!"
 | 
			
		||||
			CVT_variations = parse_at_table_directory_entry(f, font_directory.by_table.cvar, lambda f: parse_cvar_table(f, font_variations_.axisCount, control_value_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))
 | 
			
		||||
		else:
 | 
			
		||||
			CVT_variations = None
 | 
			
		||||
		if font_directory.by_table.gvar:
 | 
			
		||||
		if font_directory.has_entry(TableTag.Gvar):
 | 
			
		||||
			assert isinstance(outlines, TrueTypeOutlines)
 | 
			
		||||
			glyph_variations = parse_at_table_directory_entry(f, font_directory.by_table.gvar, lambda f: parse_gvar_table(f, outlines.glyph_data))
 | 
			
		||||
			glyph_variations = parse_at_table_directory_entry(f, font_directory.get_entry(TableTag.Gvar), lambda f: parse_gvar_table(f, outlines.glyph_data))
 | 
			
		||||
		else:
 | 
			
		||||
			glyph_variations = None
 | 
			
		||||
		horizontal_metrics_variations = possibly_parse_at_table_directory_entry(f, font_directory.by_table.HVAR, parse_HVAR_table)
 | 
			
		||||
		metrics_variations = possibly_parse_at_table_directory_entry(f, font_directory.by_table.MVAR, parse_MVAR_table)
 | 
			
		||||
		vertical_metrics_variations = possibly_parse_at_table_directory_entry(f, font_directory.by_table.VVAR, parse_VVAR_table)
 | 
			
		||||
		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)
 | 
			
		||||
 | 
			
		||||
		font_variations = FontVariations(axis_variations, CVT_variations, font_variations_, glyph_variations, horizontal_metrics_variations, metrics_variations, style_attributes, vertical_metrics_variations)
 | 
			
		||||
 | 
			
		||||
	
 | 
			
		||||
	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)
 | 
			
		||||
 | 
			
		||||
def open_font_file(file_path: str) -> OpenFontFile: # as in `open (verb) font file (noun)`, not OpenFontFile
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,6 @@
 | 
			
		||||
class ABD:
 | 
			
		||||
from abc import ABC
 | 
			
		||||
 | 
			
		||||
class ABD(ABC):
 | 
			
		||||
	"""
 | 
			
		||||
	#### Abstract Base Dataclass
 | 
			
		||||
 | 
			
		||||
@ -9,11 +11,14 @@ class ABD:
 | 
			
		||||
			msg = f"Cannot instantiate an Abstract Base Dataclass: {self.__class__.__name__}"
 | 
			
		||||
			raise TypeError(msg)
 | 
			
		||||
 | 
			
		||||
# TODO: Make a subclass of EnumMeta to do this
 | 
			
		||||
class ABE:
 | 
			
		||||
	"""
 | 
			
		||||
	#### Abstract Base Enum
 | 
			
		||||
	
 | 
			
		||||
	This is for classes that will have an Enum subclass them
 | 
			
		||||
	This is for classes that will have an Enum subclass them.
 | 
			
		||||
 | 
			
		||||
	Do not implement a __init__ method for the class directly inheriting from ABE
 | 
			
		||||
	"""
 | 
			
		||||
	def __init__(self, *args, **kwargs):
 | 
			
		||||
		if ABE in self.__class__.__bases__ or ABE == self:
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,23 @@
 | 
			
		||||
from typing import BinaryIO
 | 
			
		||||
from io import SEEK_END
 | 
			
		||||
from typing import BinaryIO, Callable, Literal, TypeVar
 | 
			
		||||
 | 
			
		||||
ENDIANNESS = 'big'
 | 
			
		||||
def len_to_end(f: BinaryIO) -> int:
 | 
			
		||||
	curr_tell = f.tell()
 | 
			
		||||
	f.seek(0, SEEK_END)
 | 
			
		||||
	end_tell = f.tell()
 | 
			
		||||
	f.seek(curr_tell)
 | 
			
		||||
	return end_tell - curr_tell
 | 
			
		||||
 | 
			
		||||
def read_int(f: BinaryIO, number: int, signed:bool=False) -> int: return int.from_bytes(f.read(number), ENDIANNESS, signed=signed)
 | 
			
		||||
def write_int(f: BinaryIO, value: int, number: int, signed:bool=False) -> int: return f.write(value.to_bytes(number, ENDIANNESS, signed=signed))
 | 
			
		||||
def is_at_end(f: BinaryIO) -> bool:
 | 
			
		||||
	return len_to_end(f) == 0
 | 
			
		||||
 | 
			
		||||
ENDIANNESS: Literal['little', 'big'] = 'big'
 | 
			
		||||
 | 
			
		||||
def read_int_from_bytes(s: bytes, *, signed:bool=False) -> int: return int.from_bytes(s, ENDIANNESS, signed=signed)
 | 
			
		||||
def bytes_from_int(value: int, number: int, *, signed:bool=False) -> bytes: return value.to_bytes(number, ENDIANNESS, signed=signed)
 | 
			
		||||
 | 
			
		||||
def read_int(f: BinaryIO, number: int, *, signed:bool=False) -> int: return read_int_from_bytes(f.read(number), signed=signed)
 | 
			
		||||
def write_int(f: BinaryIO, value: int, number: int, signed:bool=False) -> int: return f.write(bytes_from_int(value, number, signed=signed))
 | 
			
		||||
 | 
			
		||||
def read_u64(f: BinaryIO) -> int: return read_int(f, 8)
 | 
			
		||||
def read_u32(f: BinaryIO) -> int: return read_int(f, 4)
 | 
			
		||||
@ -11,7 +25,7 @@ def read_u24(f: BinaryIO) -> int: return read_int(f, 3)
 | 
			
		||||
def read_u16(f: BinaryIO) -> int: return read_int(f, 2)
 | 
			
		||||
def read_u8(f: BinaryIO) -> int: return read_int(f, 1)
 | 
			
		||||
 | 
			
		||||
def write_u16(f: BinaryIO, value: int) -> int: return f.write(value)
 | 
			
		||||
def write_u16(f: BinaryIO, value: int) -> int: return write_int(f, value, 2)
 | 
			
		||||
 | 
			
		||||
def read_i32(f: BinaryIO) -> int: return read_int(f, 4, signed=True)
 | 
			
		||||
def read_i16(f: BinaryIO) -> int: return read_int(f, 2, signed=True)
 | 
			
		||||
@ -23,9 +37,12 @@ def read_ascii(f: BinaryIO, number: int) -> str: return f.read(number).decode(en
 | 
			
		||||
def read_fixed_point(f: BinaryIO, preradix_bits: int, postradix_bits:int, *, signed:bool=True) -> float:
 | 
			
		||||
	assert (preradix_bits+postradix_bits)%8 == 0
 | 
			
		||||
	raw = read_int(f, (preradix_bits+postradix_bits)//8, signed=signed)
 | 
			
		||||
	return raw/(1<<(postradix_bits))
 | 
			
		||||
	return raw/(1<<postradix_bits)
 | 
			
		||||
 | 
			
		||||
def read_pascal_string(f: BinaryIO) -> str:
 | 
			
		||||
	string_size = read_int(f, 1)
 | 
			
		||||
	string_size = read_u8(f)
 | 
			
		||||
	pascal_string = read_ascii(f, string_size)
 | 
			
		||||
	return pascal_string
 | 
			
		||||
	return pascal_string
 | 
			
		||||
 | 
			
		||||
T = TypeVar('T')
 | 
			
		||||
Parser = Callable[[BinaryIO], T]
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
assert len(sys.argv) == 2, "usage: python3 test.py <test>"
 | 
			
		||||
assert len(sys.argv) >= 2, "usage: python3 test.py <test> [OPTIONS]"
 | 
			
		||||
 | 
			
		||||
from OpenFont import FontSpecificNameID, NameID, NameTable_Format_0, OpenFontFile, PredefinedNameID, TrueTypeOutlines, open_font_file, write_font_file
 | 
			
		||||
from OpenFont import FontSpecificNameID, NameID, NameTable_Format_0, OpenFontFile, PredefinedNameID, TrueTypeOutlines, open_font_file
 | 
			
		||||
 | 
			
		||||
def search_names(font: OpenFontFile, nameID: NameID) -> str:
 | 
			
		||||
	assert isinstance(font.naming_table, NameTable_Format_0)
 | 
			
		||||
@ -15,7 +15,7 @@ def search_names(font: OpenFontFile, nameID: NameID) -> str:
 | 
			
		||||
	assert False, f"Name not found: {nameID}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_, test = sys.argv
 | 
			
		||||
_, test, *options = sys.argv
 | 
			
		||||
 | 
			
		||||
match test:
 | 
			
		||||
	case "names":
 | 
			
		||||
@ -30,11 +30,6 @@ match test:
 | 
			
		||||
				axis_names = [search_names(font, FontSpecificNameID(axis.axisNameID)) for axis in font.font_variations.font_variations.axes]
 | 
			
		||||
				num_instances = font.font_variations.font_variations.instanceCount
 | 
			
		||||
				print(f"\tAxes: [{', '.join(axis_names)}] ({num_instances} instances)")
 | 
			
		||||
	case "rewrite":
 | 
			
		||||
		def test_font(font: OpenFontFile):
 | 
			
		||||
			PATH = "out.ttf"
 | 
			
		||||
			write_font_file(font, PATH)
 | 
			
		||||
			open_font_file(PATH)
 | 
			
		||||
	case _:
 | 
			
		||||
		assert False, f"Invalid test: '{test}'"
 | 
			
		||||
 | 
			
		||||
@ -44,13 +39,17 @@ if not os.path.exists(COMPLETED_PATH):
 | 
			
		||||
with open(COMPLETED_PATH, "r") as f: completed = f.read().split('\n')
 | 
			
		||||
 | 
			
		||||
def do_font(file: str):
 | 
			
		||||
	file = file.strip()
 | 
			
		||||
	if file in completed: return
 | 
			
		||||
	try:
 | 
			
		||||
		font = open_font_file(file)
 | 
			
		||||
		test_font(font)
 | 
			
		||||
	except AssertionError as err:
 | 
			
		||||
		err.add_note(f"Failed: {file}")
 | 
			
		||||
		raise err
 | 
			
		||||
		if '--raise' in options:
 | 
			
		||||
			err.add_note(f"Failed: {file}")
 | 
			
		||||
			raise err
 | 
			
		||||
		print(f"{file}{':' if '--no-colon' not in options else ''} {err}")
 | 
			
		||||
		return
 | 
			
		||||
	with open(COMPLETED_PATH, 'a') as f: f.write(file+'\n')
 | 
			
		||||
	completed.append(file)
 | 
			
		||||
	
 | 
			
		||||
@ -58,5 +57,3 @@ assert not sys.stdin.isatty(), f"Do not run this program directly, instead pipe
 | 
			
		||||
for line in sys.stdin:
 | 
			
		||||
	file = line.rstrip('\n')
 | 
			
		||||
	do_font(file)
 | 
			
		||||
 | 
			
		||||
print("Done!")
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user