This repository has been archived by the owner on Jun 16, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdata.py
309 lines (205 loc) · 8.8 KB
/
data.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
import re
import struct
import sys
from math import floor
from datetime import datetime, tzinfo, timedelta, timezone
class Tag:
"""
Class encapsulating an TrueType/OpenType tag data type.
- https://docs.microsoft.com/en-us/typography/opentype/spec/otff#data-types
"""
def __init__(self, string):
"""
Initialises a tag based on a string.
Ensures that a string is compliant with OpenType's tag data type.
(Must have exactly 4 characters, each character being between U+20-U+7e.)
"""
openTypeTagRegex = "[^\u0020-\u007e]"
if len(string) != 4:
raise ValueError(f"Your tag must contain exactly 4 characters. You gave {len(string)}. ('{string}')")
find = re.findall(openTypeTagRegex, string)
if len(find) > 0:
raise ValueError(f"The tag contains the following: {', '.join(find)}. This is not valid. It must only contain certain characters. These include alphanumeric characters and some symbols. (In techspeak - only in the unicode range U+20-U+7e.)")
self.tag = string
def __str__(self):
return self.tag
def __repr__(self):
return str(self.tag)
def __int__(self):
"""
Converts tag to it's expected representation in an OpenType font.
(Array of 4 UInt8s, each UInt8 representing each character's Unicode codepoint.)
ie.
"OTTO"
= 0x4F54544F
(O T T 0 )
(4F 54 54 4F)
"""
tagList = list(self.tag)
intList = [f"{ord(t):2x}" for t in tagList]
return int(intList[0] + intList[1] + intList[2] + intList[3], 16)
def toBytes(self):
"""
Returns the tag's int representation as bytes in big-endian format.
"""
return int(self).to_bytes(4, 'big')
class BFlags:
"""
Class encapsulating binary flags in font tables.
"""
def __init__(self, string):
"""
Binary flags are entered in big-endian order. (ie. left-to-right).
Input can be formatted with spaces (ie. '00100000 00001010').
Binary flags can only be 8, 16 or 32 bits long.
"""
if type(string) is not str:
raise ValueError("Making binaryFlags data type failed. Input data is not a string.")
string = string.translate({ord(' '):None}) # strip spaces
if len(string) not in [8, 16, 32]:
raise ValueError(f"Making binaryFlags data type failed. The amount of bits given was not 8, 16 or 32. It has to be one of these.")
self.len = floor(len(string)/8)
if sys.byteorder == 'little':
string = string[::-1] # reverse the byte order if system is little endian.
try:
self.bits = int(string, 2)
except ValueError as e:
raise ValueError(f"Making binaryFlags data type failed. -> {e}")
def __str__(self):
"""
Returns a string-formatted list of bits, with spacing every 8 bits.
In big-endian byte order (first to last).
"""
string = ""
bitString = f"{self.bits:0{self.len*8}b}"
if sys.byteorder == 'little': # ensure what we're working with is big-endian.
bitString = bitString[::-1]
for index, c in enumerate(bitString): # big-endian
if index%8 == 0 and index != 0: # every 8 bits, add a space.
string += ' '
string += str(c) # append the bit as a string
return string
def __repr__(self):
return str(self)
def set(self, bitNumber, value):
"""
Sets a bit to a specific binary value.
"""
self.bits = self.bits & ~(1 << bitNumber) | (value << bitNumber)
def toList(self):
"""
Returns a list of ints representing the flags, in big-endian order.
"""
list = []
bitString = f"{self.bits:0{self.len*8}b}"
if sys.byteorder == 'little': # ensure what we're working with is big-endian.
bitString = bitString[::-1]
for index, c in enumerate(bitString): # big-endian
list.append(int(c)) # append the bit as an int
return list
def toTTXStr(self):
"""
Returns a string that's little-endian formatted, for TTX use.
"""
return str(self)[::-1] # just get reverse str(), since str() guarantees big-endian.
def toBytes(self):
"""
Returns bytes in big-endian format.
"""
return self.bits.to_bytes(self.len, 'big')
class Fixed:
"""
A representation of a 'Fixed' data type in a font. This is used in normal fixed values, as well as by head.fontRevision.
(A decimal number where the two numbers on either side of the decimal represent exactly 16 bits.)
- https://docs.microsoft.com/en-us/typography/opentype/spec/otff#table-version-numbers
- https://silnrsi.github.io/FDBP/en-US/Versioning.html
"""
def __init__(self, string):
versionComponents = string.split('.')
# normal decimal versions
self.majorVersionSimple = versionComponents[0]
self.minorVersionSimple = versionComponents[1]
try:
# creating an OpenType-compliant fontRevision number based on best practices.
# https://silnrsi.github.io/FDBP/en-US/Versioning.html
self.majorVersionCalc = int(versionComponents[0])
self.minorVersionCalc = int(( int(versionComponents[1]) / 1000 ) * 65536)
except:
raise Exception("Converting headVersion to it's proper data structure failed for some reason!" + str(e))
def __str__(self):
"""
Returns a friendly, non-weird version of it.
"""
return self.majorVersionSimple + '.' + self.minorVersionSimple
def toHex(self):
"""
Returns a proper hexidecimal representation of the version number as a string.
"""
return '0x' + f"{self.majorVersionCalc:04x}" + f"{self.minorVersionCalc:04x}"
def __int__(self):
"""
Returns the proper hexadecimal representation of this value.
ie.
1.040
000010a3d
1 . 040
0001 0a3d
"""
return int(f"{self.majorVersionCalc:04x}" + f"{self.minorVersionCalc:04x}", 16)
class VFixed:
"""
A specific, non-normal representation of a fixed number, used only in certain forms of version numbers.
- https://docs.microsoft.com/en-us/typography/opentype/spec/otff#table-version-numbers
"""
def __init__(self, string):
versionComponents = string.split('.')
# normal decimal versions
self.majorVersion = int(versionComponents[0])
self.minorVersion = int(versionComponents[1])
def __int__(self):
return int(f'{self.majorVersion:>04x}' + f"{self.minorVersion:<04d}", 16)
def toHexStr(self):
return "0x" + f'{self.majorVersion:>04x}' + f"{self.minorVersion:<04d}"
def toDecimalStr(self):
return str(self.majorVersion) + '.' + str(self.minorVersion)
class LongDateTime:
"""
Class representing the LONGDATETIME data format in fonts.
LONGDATETIME is an Int64 representing the amount of seconds since 1st January 1904 at 00:00 UTC.
- https://docs.microsoft.com/en-us/typography/opentype/spec/otff#data-types
LONGDATETIME is done at UTC; there are no time zones.
"""
def __init__(self, string=None):
"""
Either takes in a formatted string representing a datetime, or nothing.
If nothing is inputted, forc will just use now.
forc's datetime format:
(Microseconds assumed to be 0.)
2019-05-22 09:59 +0000
%d-%m-%d %H:%M %z
"""
if string and string != "":
try:
self.datetime = datetime.strptime(string, "%Y-%m-%d %H:%M %z")
except:
raise ValueError(f"Creating LongDateTime data type failed. The string given ('{string}') is formatted wrong.")
else:
self.datetime = datetime.now(timezone.utc)
def __int__(self):
"""
Returns an int representation of this datetime, designed for font binary compilation.
(Returns a time delta in seconds from 1st January 1904 at 00:00 UTC to this datetime at UTC.)
"""
firstDate = datetime(1904, 1, 1, 0, 0, 0, 0, timezone.utc)
delta = self.datetime - firstDate
return floor(delta.total_seconds()) # return a rounded-down integer of the total seconds in that delta.
def toTTXStr(self):
"""
Returns a string representation of this datetime, designed for TTX compilation.
(returns a datetime string formatted in the following way:)
%a %b %d %X %Y
Wed May 22 13:45:00 2018
(24h UTC)
(Giving TTX compiler anything else will result in a TTX build error)
"""
return self.datetime.strftime("%a %b %d %X %Y")