Skip to content

Commit bd0c532

Browse files
committed
a
1 parent 31adcf5 commit bd0c532

File tree

20 files changed

+1513
-16
lines changed

20 files changed

+1513
-16
lines changed

examples/demo.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from examples import assets_dir
2+
from sfnttools.font import SfntFontCollection
3+
4+
5+
def main():
6+
example_file_path = assets_dir.joinpath('demo', 'demo.ttc')
7+
example_font = SfntFontCollection.load(example_file_path, verify_checksum=True)
8+
print()
9+
10+
11+
if __name__ == '__main__':
12+
main()

src/sfnttools/font.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
from collections import UserDict, UserList
2+
from io import BytesIO
3+
from os import PathLike
4+
from typing import Any, BinaryIO
5+
6+
from sfnttools.table import SfntTable
7+
from sfnttools.error import SfntError
8+
from sfnttools.internal.stream import Stream
9+
from sfnttools.payload import WoffPayload
10+
from sfnttools.tables import TABLE_TYPE_REGISTRY
11+
from sfnttools.tag import SfntVersion, SfntFileTag
12+
from sfnttools.woff.reader import WoffReader
13+
from sfnttools.woff2.reader import Woff2Reader, Woff2CollectionReader
14+
from sfnttools.xtf.reader import XtfReader, XtfCollectionReader
15+
16+
17+
class SfntFont(UserDict[str, SfntTable]):
18+
@staticmethod
19+
def parse(
20+
stream: bytes | BinaryIO,
21+
font_index: int | None = None,
22+
verify_checksum: bool = False,
23+
) -> 'SfntFont':
24+
if isinstance(stream, bytes):
25+
stream = BytesIO(stream)
26+
stream = Stream(stream)
27+
28+
stream.seek(0)
29+
tag = stream.read_tag()
30+
if tag == SfntFileTag.TTCF:
31+
if font_index is None:
32+
raise SfntError(f'must specify a font index in font collection')
33+
reader = XtfReader.create_by_ttc(stream, font_index, verify_checksum)
34+
elif tag == SfntFileTag.WOFF:
35+
reader = WoffReader.create(stream, verify_checksum)
36+
elif tag == SfntFileTag.WOFF2:
37+
stream.seek(4)
38+
flavor = stream.read_tag()
39+
if flavor == SfntFileTag.TTCF:
40+
if font_index is None:
41+
raise SfntError(f'must specify a font index in font collection')
42+
reader = Woff2Reader.create_by_ttc(stream, font_index)
43+
elif flavor in [*SfntVersion]:
44+
reader = Woff2Reader.create(stream, verify_checksum)
45+
else:
46+
raise SfntError('unsupported SFNT font')
47+
elif tag in [*SfntVersion]:
48+
reader = XtfReader.create(stream, verify_checksum)
49+
else:
50+
raise SfntError('unsupported SFNT font')
51+
52+
sfnt_version, tables = reader.parse_font()
53+
woff_payload = reader.read_woff_payload()
54+
return SfntFont(sfnt_version, tables, woff_payload)
55+
56+
@staticmethod
57+
def load(
58+
file_path: str | PathLike[str],
59+
font_index: int | None = None,
60+
verify_checksum: bool = False,
61+
) -> 'SfntFont':
62+
with open(file_path, 'rb') as file:
63+
return SfntFont.parse(file, font_index, verify_checksum)
64+
65+
sfnt_version: SfntVersion
66+
woff_payload: WoffPayload | None
67+
68+
def __init__(
69+
self,
70+
sfnt_version: SfntVersion,
71+
tables: dict[str, SfntTable] | None = None,
72+
woff_payload: WoffPayload | None = None,
73+
):
74+
super().__init__(tables)
75+
self.sfnt_version = sfnt_version
76+
self.woff_payload = woff_payload
77+
78+
def __setitem__(self, tag: Any, table: Any):
79+
if not isinstance(tag, str):
80+
raise KeyError("tag must be a 'str'")
81+
82+
if len(tag) != 4:
83+
raise KeyError('tag length must be 4')
84+
85+
if any(not 0x20 <= ord(c) <= 0x7E for c in tag):
86+
raise KeyError('tag contains illegal characters')
87+
88+
if tag.startswith(' '):
89+
raise KeyError('tag cannot start with a space')
90+
91+
if ' ' in tag.strip():
92+
raise KeyError('tag cannot contain spaces in between')
93+
94+
table_type = TABLE_TYPE_REGISTRY.get(tag, SfntTable)
95+
if not isinstance(table, table_type):
96+
raise ValueError(f'bad table type for tag {repr(tag)}')
97+
98+
super().__setitem__(tag, table)
99+
100+
def __repr__(self) -> str:
101+
return object.__repr__(self)
102+
103+
104+
class SfntFontCollection(UserList[SfntFont]):
105+
@staticmethod
106+
def parse(
107+
stream: bytes | BinaryIO,
108+
share_tables: bool = True,
109+
verify_checksum: bool = False,
110+
) -> 'SfntFontCollection':
111+
if isinstance(stream, bytes):
112+
stream = BytesIO(stream)
113+
stream = Stream(stream)
114+
115+
stream.seek(0)
116+
tag = stream.read_tag()
117+
if tag == SfntFileTag.TTCF:
118+
collection_reader = XtfCollectionReader.create(stream, share_tables, verify_checksum)
119+
elif tag == SfntFileTag.WOFF2:
120+
stream.seek(4)
121+
flavor = stream.read_tag()
122+
if flavor != SfntFileTag.TTCF:
123+
raise SfntError('not a woff2 collection font')
124+
collection_reader = Woff2CollectionReader.create(stream, share_tables)
125+
else:
126+
raise SfntError('unsupported SFNT collection font')
127+
128+
fonts = []
129+
for font_index in range(collection_reader.get_num_fonts()):
130+
reader = collection_reader.create_reader(font_index)
131+
sfnt_version, tables = reader.parse_font()
132+
fonts.append(SfntFont(sfnt_version, tables))
133+
woff_payload = collection_reader.read_woff_payload()
134+
return SfntFontCollection(fonts, woff_payload)
135+
136+
@staticmethod
137+
def load(
138+
file_path: str | PathLike[str],
139+
share_tables: bool = True,
140+
verify_checksum: bool = False,
141+
) -> 'SfntFontCollection':
142+
with open(file_path, 'rb') as file:
143+
return SfntFontCollection.parse(file, share_tables, verify_checksum)
144+
145+
woff_payload: WoffPayload | None
146+
147+
def __init__(
148+
self,
149+
fonts: list[SfntFont] | None = None,
150+
woff_payload: WoffPayload | None = None,
151+
):
152+
super().__init__(fonts)
153+
self.woff_payload = woff_payload
154+
155+
def __repr__(self) -> str:
156+
return object.__repr__(self)

src/sfnttools/internal/checksum.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from sfnttools.error import SfntError
1+
from typing import Final
2+
3+
CHECKSUM_MASK: Final = 0xFFFFFFFF
24

35

46
def calculate_checksum(data: bytes) -> int:
@@ -8,17 +10,5 @@ def calculate_checksum(data: bytes) -> int:
810
if len(chunk) < 4:
911
chunk += b'\x00' * (4 - len(chunk))
1012
checksum += int.from_bytes(chunk, 'big', signed=False)
11-
checksum &= 0xFFFFFFFF
13+
checksum &= CHECKSUM_MASK
1214
return checksum
13-
14-
15-
def calculate_table_checksum(tag: str, data: bytes):
16-
if tag == 'head':
17-
data = data[:8] + b'\x00\x00\x00\x00' + data[12:]
18-
return calculate_checksum(data)
19-
20-
21-
def verify_table_checksum(tag: str, data: bytes, expected: int):
22-
checksum = calculate_table_checksum(tag, data)
23-
if checksum != expected:
24-
raise SfntError(f'table {repr(tag)} bad checksum')

src/sfnttools/internal/stream.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
from io import BytesIO
23
from typing import BinaryIO
34

@@ -196,8 +197,8 @@ def write_nulls(self, size: int) -> int:
196197
def align_to_4_byte_with_nulls(self) -> int:
197198
return self.write_nulls(3 - (self.tell() + 3) % 4)
198199

199-
def seek(self, offset: int):
200-
self.source.seek(offset)
200+
def seek(self, offset: int, whence: int = os.SEEK_SET):
201+
self.source.seek(offset, whence)
201202

202203
def tell(self) -> int:
203204
return self.source.tell()

src/sfnttools/payload.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
2+
class WoffPayload:
3+
major_version: int
4+
minor_version: int
5+
metadata: bytes | None
6+
private_data: bytes | None
7+
8+
def __init__(
9+
self,
10+
major_version: int = 0,
11+
minor_version: int = 0,
12+
metadata: bytes | None = None,
13+
private_data: bytes | None = None,
14+
):
15+
self.major_version = major_version
16+
self.minor_version = minor_version
17+
self.metadata = metadata
18+
self.private_data = private_data

0 commit comments

Comments
 (0)