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
|
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
#
osLibFound = False
sysLibFound = False
shutilLibFound = False
structLibFound = False
imagePilLibFound = False
try:
import os
except ImportError:
print "[Error] os python library is required to be installed!"
else:
osLibFound = True
try:
import sys
except ImportError:
print "[Error] sys python library is required to be installed!"
else:
sysLibFound = True
try:
import struct
except ImportError:
print "[Error] struct python library is required to be installed!"
else:
structLibFound = True
try:
from PIL import Image
except ImportError:
print "[Error] Image python library (PIL) is required to be installed!"
else:
imagePilLibFound = True
if (not osLibFound) \
or (not sysLibFound) \
or (not structLibFound) \
or (not imagePilLibFound):
sys.stdout.write("[Error] Errors were found when trying to import required python libraries\n")
sys.exit(1)
from struct import *
MY_MODULE_VERSION = "0.80"
MY_MODULE_NAME = "fonFileLib"
class FonHeader(object):
maxEntriesInTableOfDetails = -1 # this is probably always the number of entries in table of details, but it won't work for the corrupted TAHOMA18.FON file
maxGlyphWidth = -1 # in pixels
maxGlyphHeight = -1 # in pixels
graphicSegmentByteSize = -1 # Graphic segment byte size
def __init__(self):
return
class fonFile(object):
m_header = FonHeader()
simpleFontFileName = 'GENERIC.FON'
realNumOfCharactersInImageSegment = 0 # this is used for the workaround for the corrupted TAHOME18.FON
nonEmptyCharacters = 0
glyphDetailEntriesLst = [] # list of 5-value tuples. Tuple values are (X-offset, Y-offset, Width, Height, Offset in Graphics segment)
glyphPixelData = None # buffer of pixel data for glyphs
m_traceModeEnabled = False
# traceModeEnabled is bool to enable more printed debug messages
def __init__(self, traceModeEnabled = True):
del self.glyphDetailEntriesLst[:]
self.glyphPixelData = None # buffer of pixel data for glyphs
self.simpleFontFileName = 'GENERIC.FON'
self.realNumOfCharactersInImageSegment = 0 # this is used for the workaround for the corrupted TAHOME18.FON
self.nonEmptyCharacters = 0
self.m_traceModeEnabled = traceModeEnabled
return
def loadFonFile(self, fonBytesBuff, maxLength, fonFileName):
self.simpleFontFileName = fonFileName
offsInFonFile = 0
localLstOfDataOffsets = []
del localLstOfDataOffsets[:]
#
# parse FON file fields for header
#
try:
tmpTuple = struct.unpack_from('I', fonBytesBuff, offsInFonFile) # unsigned integer 4 bytes
self.header().maxEntriesInTableOfDetails = tmpTuple[0]
offsInFonFile += 4
if self.simpleFontFileName == 'TAHOMA18.FON': # deal with corrupted original 'TAHOMA18.FON' file
self.realNumOfCharactersInImageSegment = 176
if self.m_traceModeEnabled:
print "[Debug] SPECIAL CASE. WORKAROUND FOR CORRUPTED %s FILE. Only %d characters supported!" % (self.simpleFontFileName, self.realNumOfCharactersInImageSegment)
else:
self.realNumOfCharactersInImageSegment = self.header().maxEntriesInTableOfDetails
tmpTuple = struct.unpack_from('I', fonBytesBuff, offsInFonFile) # unsigned integer 4 bytes
self.header().maxGlyphWidth = tmpTuple[0]
offsInFonFile += 4
tmpTuple = struct.unpack_from('I', fonBytesBuff, offsInFonFile) # unsigned integer 4 bytes
self.header().maxGlyphHeight = tmpTuple[0]
offsInFonFile += 4
tmpTuple = struct.unpack_from('I', fonBytesBuff, offsInFonFile) # unsigned integer 4 bytes
self.header().graphicSegmentByteSize = tmpTuple[0]
offsInFonFile += 4
if self.m_traceModeEnabled:
print "[Debug] Font file (FON) Header Info: "
print "[Debug] Number of entries: %d, Glyph max-Width: %d, Glyph max-Height: %d, Graphic Segment size: %d" % (self.header().maxEntriesInTableOfDetails, self.header().maxGlyphWidth, self.header().maxGlyphHeight, self.header().graphicSegmentByteSize)
#
# Glyph details table (each entry is 5 unsigned integers == 5*4 = 20 bytes)
# For most characters, their ASCII value + 1 is the index of their glyph's entry in the details table. The 0 entry of this table is reserved
#
#tmpXOffset, tmpYOffset, tmpWidth, tmpHeight, tmpDataOffset
if self.m_traceModeEnabled:
print "[Debug] Font file (FON) glyph details table: "
for idx in range(0, self.realNumOfCharactersInImageSegment):
tmpTuple = struct.unpack_from('i', fonBytesBuff, offsInFonFile) # unsigned integer 4 bytes
tmpXOffset = tmpTuple[0]
offsInFonFile += 4
tmpTuple = struct.unpack_from('I', fonBytesBuff, offsInFonFile) # unsigned integer 4 bytes
tmpYOffset = tmpTuple[0]
offsInFonFile += 4
tmpTuple = struct.unpack_from('I', fonBytesBuff, offsInFonFile) # unsigned integer 4 bytes
tmpWidth = tmpTuple[0]
offsInFonFile += 4
tmpTuple = struct.unpack_from('I', fonBytesBuff, offsInFonFile) # unsigned integer 4 bytes
tmpHeight = tmpTuple[0]
offsInFonFile += 4
tmpTuple = struct.unpack_from('I', fonBytesBuff, offsInFonFile) # unsigned integer 4 bytes
tmpDataOffset = tmpTuple[0]
offsInFonFile += 4
if tmpWidth == 0 or tmpHeight == 0:
if self.m_traceModeEnabled:
print "Index: %d\t UNUSED *****************************************************************" % (idx)
else:
self.nonEmptyCharacters += 1
if self.m_traceModeEnabled:
print "Index: %d\txOffs: %d\tyOffs: %d\twidth: %d\theight: %d\tdataOffs: %d" % (idx, tmpXOffset, tmpYOffset, tmpWidth, tmpHeight, tmpDataOffset)
if tmpDataOffset not in localLstOfDataOffsets:
localLstOfDataOffsets.append(tmpDataOffset)
else:
# This never happens in the original files. Offsets are "re-used" but not really because it happens only for empty (height = 0) characters which all seem to point to the next non-empty character
if self.m_traceModeEnabled:
print "Index: %d\t RE-USING ANOTHER GLYPH *****************************************************************" % (idx)
self.glyphDetailEntriesLst.append( ( tmpXOffset, tmpYOffset, tmpWidth, tmpHeight, tmpDataOffset) )
offsInFonFile = (4 * 4) + (self.header().maxEntriesInTableOfDetails * 5 * 4) # we need the total self.header().maxEntriesInTableOfDetails here and not self.realNumOfCharactersInImageSegment
self.glyphPixelData = fonBytesBuff[offsInFonFile:]
return True
except:
print "[Error] Loading Font file (FON) %s failed!" % (self.simpleFontFileName)
raise
return False
def outputFonToPNG(self):
print "[Info] Exporting font file (FON) to PNG: %s" % (self.simpleFontFileName + ".PNG")
targWidth = 0
targHeight = 0
paddingFromTopY = 2
paddingBetweenGlyphsX = 10
if len(self.glyphDetailEntriesLst) == 0 or (len(self.glyphDetailEntriesLst) != self.realNumOfCharactersInImageSegment and len(self.glyphDetailEntriesLst) != self.header().maxEntriesInTableOfDetails) :
print "[Error] Font file (FON) loading process did not complete correctly. Missing important data in structures. Cannot output image!"
return
# TODO asdf refine this code here. the dimensions calculation is very crude for now
if self.header().maxGlyphWidth > 0 :
targWidth = (self.header().maxGlyphWidth + paddingBetweenGlyphsX) * (self.realNumOfCharactersInImageSegment + 1)
else:
targWidth = 1080
# TODO asdf refine this code here. the dimensions calculation is very crude for now
if self.header().maxGlyphHeight > 0 :
targHeight = self.header().maxGlyphHeight * 2
else:
targHeight = 480
imTargetGameFont = Image.new("RGBA",(targWidth, targHeight), (0,0,0,0))
#print imTargetGameFont.getbands()
#
# Now fill in the image segment
# Fonts in image segment are stored in pixel colors from TOP to Bottom, Left to Right per GLYPH.
# Each pixel is 16 bit (2 bytes). Highest bit seems to determine transparency (on/off flag).
# There seem to be 5 bits per RGB channel and the value is the corresponding 8bit value (from the 24 bit pixel color) shifting out (right) the 3 LSBs
# First font image is the special character (border of top row and left column) - color of font pixels should be "0x7FFF" for filled and "0x8000" for transparent
drawIdx = 0
drawIdxDeductAmount = 0
for idx in range(0, self.realNumOfCharactersInImageSegment):
# TODO check for size > 0 for self.glyphPixelData
# TODO mark glyph OUTLINES? (optional by switch)
(glyphXoffs, glyphYoffs, glyphWidth, glyphHeight, glyphDataOffs) = self.glyphDetailEntriesLst[idx]
glyphDataOffs = glyphDataOffs * 2
#print idx, glyphDataOffs
currX = 0
currY = 0
if (glyphWidth == 0 or glyphHeight == 0):
drawIdxDeductAmount += 1
drawIdx = idx - drawIdxDeductAmount
for colorIdx in range(0, glyphWidth*glyphHeight):
tmpTuple = struct.unpack_from('H', self.glyphPixelData, glyphDataOffs) # unsigned short 2 bytes
pixelColor = tmpTuple[0]
glyphDataOffs += 2
# if pixelColor > 0x8000:
# print "[Debug] WEIRD CASE" # NEVER HAPPENS - TRANSPARENCY IS ON/OFF. There's no grades of transparency
rgbacolour = (0,0,0,0)
if pixelColor == 0x8000:
rgbacolour = (0,0,0,0) # alpha: 0.0 fully transparent
else:
tmp8bitR1 = ( (pixelColor >> 10) ) << 3
tmp8bitG1 = ( (pixelColor & 0x3ff) >> 5 ) << 3
tmp8bitB1 = ( (pixelColor & 0x1f) ) << 3
rgbacolour = (tmp8bitR1,tmp8bitG1,tmp8bitB1, 255) # alpha: 1.0 fully opaque
#rgbacolour = (255,255,255, 255) # alpha: 1.0 fully opaque
if currX == glyphWidth:
currX = 0
currY += 1
imTargetGameFont.putpixel(( (drawIdx + 1) * (self.header().maxGlyphWidth + paddingBetweenGlyphsX ) + currX, paddingFromTopY + glyphYoffs + currY), rgbacolour)
currX += 1
try:
imTargetGameFont.save(os.path.join('.', self.simpleFontFileName + ".PNG"), "PNG")
except Exception as e:
print '[Error] Unable to write to output PNG file. ' + str(e)
def header(self):
return self.m_header
#
#
#
if __name__ == '__main__':
# main()
errorFound = False
# By default assumes a file of name SUBTLS_E.FON in same directory
# otherwise tries to use the first command line argument as input file
# 'TAHOMA24.FON' # USED IN CREDIT END-TITLES and SCORERS BOARD AT POLICE STATION
# 'TAHOMA18.FON' # USED IN CREDIT END-TITLES
# '10PT.FON' # BLADE RUNNER UNUSED FONT - Probably font for reporting system errors
# 'KIA6PT.FON' # BLADE RUNNER MAIN FONT
# 'SUBTLS_E.FON' # OUR EXTRA FONT USED FOR SUBTITLES
inFONFile = None
inFONFileName = 'SUBTLS_E.FON' # Subtitles font custom
if len(sys.argv[1:]) > 0 \
and os.path.isfile(os.path.join('.', sys.argv[1])) \
and len(sys.argv[1]) >= 5 \
and sys.argv[1][-3:].upper() == 'FON':
inFONFileName = sys.argv[1]
print "[Info] Attempting to use %s as input FON file..." % (inFONFileName)
elif os.path.isfile(os.path.join('.', inFONFileName)):
print "[Info] Using default %s as input FON file..." % (inFONFileName)
else:
print "[Error] No valid input file argument was specified and default input file %s is missing." % (inFONFileName)
errorFound = True
if not errorFound:
try:
print "[Info] Opening %s" % (inFONFileName)
inFONFile = open(os.path.join('.',inFONFileName), 'rb')
except:
errorFound = True
print "[Error] Unexpected event:", sys.exc_info()[0]
raise
if not errorFound:
allOfFonFileInBuffer = inFONFile.read()
fonFileInstance = fonFile(True)
if fonFileInstance.m_traceModeEnabled:
print "[Debug] Running %s (%s) as main module" % (MY_MODULE_NAME, MY_MODULE_VERSION)
if (fonFileInstance.loadFonFile(allOfFonFileInBuffer, len(allOfFonFileInBuffer), inFONFileName)):
print "[Info] Font file (FON) was loaded successfully!"
fonFileInstance.outputFonToPNG()
else:
print "[Error] Error while loading Font file (FON)!"
inFONFile.close()
else:
#debug
#print "[Debug] Running %s imported from another module" % (MY_MODULE_NAME)
pass
|