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 pathfont.py
353 lines (235 loc) · 10.9 KB
/
font.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
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
from lxml.etree import Element, tostring
from math import log2, floor
from transform.bytes import calculateTableChecksum, generateOffsets
import struct
import log
from format import formats
from data import Tag
import tables.tableRecord
import tables.glyphOrder
import tables.head
import tables.os2
import tables.post
import tables.name
import tables.maxp
import tables.gasp
import tables.loca
import tables.dsig
import tables.hhea
import tables.hmtx
import tables.vhea
import tables.vmtx
import tables.cmap
# import tables.gdef
# import tables.gpos
import tables.gsub
# import tables.morx
import tables.glyf
import tables.svg
import tables.sbix
import tables.cbdt
import tables.cblc
class TTFont:
"""
Class representing a TrueType/OpenType font.
"""
def __init__(self, chosenFormat, m, glyphs, flags):
"""
Covers the entire routine for assembling a TrueType/OpenType font with forc input data.
"""
glyphFormat = formats[chosenFormat]["imageTables"]
self.tables = {}
try:
# not actually tables
# ---------------------------------------------
self.glyphOrder = tables.glyphOrder.GlyphOrder(glyphs)
# headers and other weird crap
# ---------------------------------------------
log.out('[head] ', 90, newline=False)
self.tables["head"] = tables.head.head(m)
log.out('[OS/2] ', 90, newline=False)
self.tables["OS/2"] = tables.os2.OS2(m, glyphs)
#log.out('[post] ', 90, newline=False)
#self.tables.append(tables.post.post(glyphs))
# maxp is a semi-placeholder table.
log.out('[maxp] ', 90, newline=False)
self.tables["maxp"] = tables.maxp.maxp(glyphs)
log.out('[gasp] ', 90, newline=False)
self.tables["gasp"] = tables.gasp.gasp()
# loca is a placeholder to make macOS happy.
#
# CBDT/CBLC either doesn't use loca or TTX doesn't want
# an empty loca table if there's no glyf table (CBDT/CBLC
# fonts shouldnt have glyf tables.)
if glyphFormat is not "CBx":
log.out('[loca] ', 36, newline=False)
self.tables["loca"] = tables.loca.loca()
# placeholder table that makes Google's font validation happy.
log.out('[DSIG]', 90)
self.tables["DSIG"] = tables.dsig.DSIG()
# horizontal and vertical metrics tables
# ---------------------------------------------
log.out('[hhea] ', 90, newline=False)
self.tables["hhea"] = tables.hhea.hhea(m)
log.out('[hmtx] ', 90, newline=False)
self.tables["hmtx"] = tables.hmtx.hmtx(m, glyphs)
log.out('[vhea] ', 90, newline=False)
self.tables["vhea"] = tables.vhea.vhea(m)
log.out('[vmtx]', 90)
self.tables["vmtx"] = tables.vmtx.vmtx(m, glyphs)
# glyph-code mappings
# ---------------------------------------------
# single glyphs
log.out('[cmap] ', 90, newline=False)
self.tables["cmap"] = tables.cmap.cmap(glyphs, flags["no_vs16"])
# ligatures
ligatures = False
# check for presence of ligatures
for g in glyphs:
if len(g) > 1:
ligatures = True
if ligatures:
if formats[chosenFormat]["ligatureFormat"] == "OpenType":
log.out('[GSUB] ', 36, newline=False)
self.tables["GSUB"] = tables.gsub.GSUB(glyphs)
# glyf
# ---------------------------------------------
# glyf is used as a placeholde to please font validation,
# table dependencies and the TTX compiler.
#
# CBDT/CBLC doesn't use glyf at all
if glyphFormat is not "CBx":
log.out('[glyf] ', 36, newline=False)
self.tables["glyf"] = tables.glyf.glyf(m, glyphs)
# actual glyph picture data
# ---------------------------------------------
if glyphFormat == "SVG":
log.out('[SVG ]', 36)
self.tables["SVG "] = tables.svg.SVG(m, glyphs)
elif glyphFormat == "sbix":
log.out('[sbix]', 36)
self.tables["sbix"] = tables.sbix.sbix(glyphs)
elif glyphFormat == "CBx":
log.out('[CBLC] ', 36, newline=False)
self.tables["CBLC"] = tables.cblc.CBLC(m, glyphs)
log.out('[CBDT]', 36)
self.tables["CBDT"] = tables.cbdt.CBDT(m, glyphs)
# human-readable metadata
# ---------------------------------------------
log.out('[name]', 90)
self.tables["name"] = tables.name.name(chosenFormat, m)
except ValueError as e:
ValueError(f"Something went wrong with building the font class. -> {e}")
def test(self):
"""
A series of tests determining the validity of the font, checking certain
variables between font tables that must agree with each other (as opposed to
checking issues that exist solely within a certain table).
"""
# certain bits in head.macStyle and OS/2.fsSelection must agree with each other.
# ------------------------------------------------------------------------------
macStyleBold = self.tables["head"].macStyle.toList()[0]
fsSelectionBold = self.tables["OS/2"].fsSelection.toList()[5]
macStyleItalic = self.tables["head"].macStyle.toList()[1]
fsSelectionItalic = self.tables["OS/2"].fsSelection.toList()[0]
if macStyleBold != fsSelectionBold:
log.out(f"💢 The Bold bit in head.macStyle (bit 0: {macStyleBold}) does not agree with the Bold bit in OS/2.fsSelection (bit 5: {fsSelectionBold})", 91)
if macStyleItalic != fsSelectionItalic:
log.out(f"💢 The Italic bit in head.macStyle (bit 1: {macStyleItalic}) does not agree with the Italic bit in OS/2.fsSelection (bit 0: {fsSelectionItalic})", 91)
# number of glyphs in an sbix strike must be equal to maxp.numGlyphs.
# ------------------------------------------------------------------------------
maxpNumGlyphs = self.tables["maxp"].numGlyphs
if "sbix" in self.tables:
strikes = self.tables["sbix"].strikes
for num, s in enumerate(strikes):
if len(s.bitmaps) != self.tables["maxp"].numGlyphs:
log.out(f"💢 the number of bitmaps inside sbix strike index {num} (ppem: {s.ppem}, ppi: {s.ppi}) doesn't match maxp.numGlyphs. (sbix strike: {len(s.bitmaps)}, maxp.numGlyphs: {self.tables['maxp'].numGlyphs}).", 91)
def toTTX(self, asString=False):
"""
Compiles font class to a TTX-formatted string.
"""
# start the TTX file
# ---------------------------------------------
root = Element('ttFont', {'sfntVersion': '\\x00\\x01\\x00\\x00', 'ttLibVersion': '3.28'}) # hard-coded attrs.
# get all of this font's tables' TTX representations and append them to the file.
root.append(self.glyphOrder.toTTX())
for tableName, t in self.tables.items():
root.append(t.toTTX())
# the TTX is now done! (as long as something didn't go wrong)
# choose whether to get the result as a formatted string or as an lxml Element.
if asString:
return tostring(root, pretty_print=True, method="xml", xml_declaration=True, encoding="UTF-8")
else:
return root
def bytesPass(self):
"""
Represents a single compile pass to bytes.
(Just a WIP/placeholder right now.)
"""
# offset table (ie. the font header)
# --------------------------------------------------------------
# (this should be fine and complete)
numTables = len(self.tables)
searchRange = (2 ** floor(log2(numTables))) * 16
entrySelector = int(log2(floor(log2(numTables))))
rangeShift = numTables * 16 - searchRange
offsetTable = struct.pack( ">IHHHH"
, 0x00010000 # sfntVersion, UInt32
, numTables # UInt16
, searchRange # UInt16
, entrySelector # UInt16
, rangeShift # UInt16
)
# table record entries
# -------------------------------------------------------------
initialTables = []
originalLengths = []
checkSums = []
tags = []
# get all of the table data
for tableName, t in self.tables.items():
#print(f"converting {tableName} to bytes...")
# convert to bytes
try:
tableOutput = t.toBytes()
except ValueError as e:
raise ValueError(f"Something has gone wrong with converting the {tableName} table to bytes. -> {e}")
initialTables.append(tableOutput[0])
originalLengths.append(tableOutput[1])
# get a checksum on that data
try:
checkSums.append(calculateTableChecksum(tableOutput[0]))
except ValueError as e:
raise ValueError(f"Something has gone wrong with calculating the checksum for {tableName}. -> {e}")
# also add a tag.
tags.append(tableName)
# calculate offsets for each table
initialOffset = (len(self.tables) * 16) + 12 # 16 = tableRecord length, 12 = offset table length.
tableOffsets = generateOffsets(initialTables, 32, initialOffset, usingClasses=False)
tableRecordsList = []
for n, t in enumerate(initialTables):
tableRecordsList.append(tables.tableRecord.TableRecord( tags[n]
, checkSums[n]
, tableOffsets["offsetInts"][n]
, originalLengths[n]
))
#print(tableRecordsList)
tableRecordsList.sort()
tableRecords = b''
for t in tableRecordsList:
tableRecords += t.toBytes()
return offsetTable + tableRecords + tableOffsets["bytes"]
def toBytes(self):
"""
Compiles font class into a fully formed TrueType/OpenType font.
(WIP)
"""
log.out('first compilation pass...', 90)
firstPass = self.bytesPass()
log.out('calculating checksum...', 90)
initialCS = calculateTableChecksum(firstPass)
checkSumAdjustment = (0xB1B0AFBA - initialCS) % 0x100000000
self.tables["head"].checkSumAdjustment = checkSumAdjustment
log.out('last compilation pass...', 90)
lastPass = self.bytesPass()
return lastPass