Skip to content

Commit e5f4909

Browse files
committed
a
1 parent 09ba792 commit e5f4909

File tree

24 files changed

+1606
-16
lines changed

24 files changed

+1606
-16
lines changed

examples/demo.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from examples import assets_dir
2+
from sfnttools.font import SfntFont
3+
4+
5+
def main():
6+
SfntFont.load(assets_dir.joinpath('source-han-sans', 'SourceHanSansSC-VF.otf'), verify_checksum=True)
7+
SfntFont.load(assets_dir.joinpath('source-han-sans', 'SourceHanSansSC-VF.otf.woff2'), verify_checksum=True)
8+
SfntFont.load(assets_dir.joinpath('source-han-sans', 'SourceHanSansSC-VF.ttf'), verify_checksum=True)
9+
SfntFont.load(assets_dir.joinpath('source-han-sans', 'SourceHanSansSC-VF.ttf.woff2'), verify_checksum=True)
10+
11+
a = SfntFont.load(assets_dir.joinpath('demo', 'demo.otf'), verify_checksum=True)
12+
b = SfntFont.load(assets_dir.joinpath('demo', 'demo.otf.woff'), verify_checksum=True)
13+
c = SfntFont.load(assets_dir.joinpath('demo', 'demo.otf.woff2'), verify_checksum=True)
14+
d = SfntFont.load(assets_dir.joinpath('demo', 'demo.ttf'), verify_checksum=True)
15+
e = SfntFont.load(assets_dir.joinpath('demo', 'demo.ttf.woff'), verify_checksum=True)
16+
f = SfntFont.load(assets_dir.joinpath('demo', 'demo.ttf.woff2'), verify_checksum=True)
17+
18+
print()
19+
20+
21+
if __name__ == '__main__':
22+
main()

src/sfnttools/checksum.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
_CHECKSUM_MASK = 0xFFFFFFFF
3+
_CHECKSUM_MAGIC_NUMBER = 0xB1B0AFBA
4+
5+
6+
def calculate_checksum(data: bytes) -> int:
7+
checksum = 0
8+
for i in range(0, len(data), 4):
9+
chunk = data[i:i + 4]
10+
if len(chunk) < 4:
11+
chunk += b'\x00' * (4 - len(chunk))
12+
checksum += int.from_bytes(chunk, 'big', signed=False)
13+
checksum &= _CHECKSUM_MASK
14+
return checksum
15+
16+
17+
def calculate_checksum_adjustment(checksums: list[int]) -> int:
18+
total_checksum = sum(checksums) & _CHECKSUM_MASK
19+
checksum_adjustment = (_CHECKSUM_MAGIC_NUMBER - total_checksum) & _CHECKSUM_MASK
20+
return checksum_adjustment

src/sfnttools/error.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
class SfntError(Exception):
3+
pass

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.error import SfntError
7+
from sfnttools.payload import WoffPayload
8+
from sfnttools.stream import Stream
9+
from sfnttools.table import SfntTable
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 font')
47+
elif tag in [*SfntVersion]:
48+
reader = XtfReader.create(stream, verify_checksum)
49+
else:
50+
raise SfntError('unsupported 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 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: 0 additions & 14 deletions
This file was deleted.

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
File renamed without changes.

0 commit comments

Comments
 (0)